作者介绍:✌️大厂全栈码农|毕设实战开发,专注于大学生项目实战开发、讲解和毕业答疑辅导。

 推荐订阅精彩专栏  避免错过下次更新

Springboot项目精选实战案例

更多项目:CSDN主页YAML墨韵

学如逆水行舟,不进则退。学习如赶路,不能慢一步。

目录

1、描述

2、pom.xml文件

3、创建redis工具类

4、创建rabbitmq的配置类

5、创建数据库表

用户信息 

商品信息 

秒杀信息

订单信息

秒杀订单 

6、具体实现过程

用户登录

秒杀商品数量初始化

rabbitmq队列

生产者代码

消费者代码

订单模块

注意事项:

秒杀功能作为大型交易平台的常见活动,落地实现的时候需要应对大量并发请求,同时保证请求的快速、准确处理。本文通过案例分析,讲解如何结合Springboot 3和RabbitMQ和Redis来构建秒杀请求的异步处理队列,并通过性能测试对异步处理方案进行优化。

1、描述

交易平台秒杀功能的业务流程

秒杀活动一般发生在一些特定的时间点,如节日特卖或者是限量产品的销售。这样的活动通常会吸引大批用户的参与。由于参与的用户量大,再加上秒杀商品的数量有限,因此这种活动对后端系统的架构设计提出了很大挑战。一般来说,秒杀活动的流程可以分为以下几个步骤:

秒杀宣传与倒计时:商家通过广告和营销方式进行秒杀活动的宣传,并在秒杀页面显示一个倒计时,告知用户秒杀活动开始的时间。 用户抢单:一般来说,用户需要在秒杀页面上点击“立即秒杀”按钮,发起秒杀请求。 系统处理秒杀请求:由于秒杀活动瞬间会产生大量用户请求,所以系统要有相应的优化措施。这里我们采用的是异步处理的方式。具体来说,就是当用户发起秒杀请求后,实际上用户的请求被发送到RabbitMQ的消息队列中,然后通过后台服务进行异步处理。 检查库存:在进行后续操作之前,系统会检查当前商品的库存量,以确保没有超卖。 扣减库存并生成订单:这是一个原子操作,即系统需要在一次操作中完成库存扣减和订单生成。也就是说,当我们更新库存数量的同时,也会在订单表中创建新的订单记录。 支付处理:成功生成订单的用户被引导至支付页面进行付款操作。支付通常有一定的时间限制,如果超过时间未支付,订单会被自动取消,并将库存加回。 订单处理:用户成功支付后,系统会进行后续的订单处理工作,比如发货等。

以上是一个通用的秒杀活动业务流程。实际上,在面对大流量的情况下,需要利用各种手段进行优化,以保证服务的稳定性和用户的体验,比如引入消息队列进行异步处理,使用缓存等。具体的优化手段,还需要根据业务场景和系统状况灵活选择和实施。

2、pom.xml文件

RabbitMQ的消息队列配置请看:Spring Boot与RabbitMQ整合:实现高可用消息队列服务

redis的基本配置和实战请看:Spring Boot与Redis深度整合:实战指南

我们需要在SpringBoot应用中集成RabbitMQ和Redis。我们在pom.xml文件中添加依赖:

org.springframework.boot

spring-boot-starter-amqp

org.springframework.boot

spring-boot-starter-data-redis

在application.yml中配置RabbitMQ和Redis:

spring:

rabbitmq:

host: your-rabbitmq-host

port: 5672

username: guest

password: guest

redis:

host: your-redis-host

port: 6379

password: your-redis-password # 如果设置了密码的话

3、创建redis工具类

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import org.springframework.util.CollectionUtils;

import java.util.Collection;

import java.util.List;

import java.util.Map;

import java.util.Set;

import java.util.concurrent.TimeUnit;

@Component

public class RedisUtil {

@Autowired

private RedisTemplate redisTemplate;

/**

* 指定缓存失效时间

* @param key 键

* @param time 时间(秒)

*/

public boolean expire(String key, long time) {

try {

if (time > 0) {

redisTemplate.expire(key, time, TimeUnit.SECONDS);

}

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 根据key 获取过期时间

* @param key 键 不能为null

* @return 时间(秒) 返回0代表为永久有效

*/

public long getExpire(String key) {

return redisTemplate.getExpire(key, TimeUnit.SECONDS);

}

/**

* 判断key是否存在

* @param key 键

* @return true 存在 false不存在

*/

public boolean hasKey(String key) {

try {

return redisTemplate.hasKey(key);

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 删除缓存

* @param key 可以传一个值 或多个

*/

@SuppressWarnings("unchecked")

public void del(String... key) {

if (key != null && key.length > 0) {

if (key.length == 1) {

redisTemplate.delete(key[0]);

} else {

redisTemplate.delete((Collection) CollectionUtils.arrayToList(key));

}

}

}

// ============================String=============================

/**

* 普通缓存获取

* @param key 键

* @return 值

*/

public Object get(String key) {

return key == null ? null : redisTemplate.opsForValue().get(key);

}

/**

* 普通缓存放入

* @param key 键

* @param value 值

* @return true成功 false失败

*/

public boolean set(String key, Object value) {

try {

redisTemplate.opsForValue().set(key, value);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 普通缓存放入并设置时间

* @param key 键

* @param value 值

* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期

* @return true成功 false 失败

*/

public boolean set(String key, Object value, long time) {

try {

if (time > 0) {

redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);

} else {

set(key, value);

}

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 递增

* @param key 键

* @param delta 要增加几(大于0)

*/

public long incr(String key, long delta) {

if (delta < 0) {

throw new RuntimeException("递增因子必须大于0");

}

return redisTemplate.opsForValue().increment(key, delta);

}

/**

* 递减

* @param key 键

* @param delta 要减少几(小于0)

*/

public long decr(String key, long delta) {

if (delta < 0) {

throw new RuntimeException("递减因子必须大于0");

}

return redisTemplate.opsForValue().increment(key, -delta);

}

// ================================Map=================================

/**

* HashGet

* @param key 键 不能为null

* @param item 项 不能为null

*/

public Object hget(String key, String item) {

return redisTemplate.opsForHash().get(key, item);

}

/**

* 获取hashKey对应的所有键值

* @param key 键

* @return 对应的多个键值

*/

public Map hmget(String key) {

return redisTemplate.opsForHash().entries(key);

}

/**

* HashSet

* @param key 键

* @param map 对应多个键值

*/

public boolean hmset(String key, Map map) {

try {

redisTemplate.opsForHash().putAll(key, map);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* HashSet 并设置时间

* @param key 键

* @param map 对应多个键值

* @param time 时间(秒)

* @return true成功 false失败

*/

public boolean hmset(String key, Map map, long time) {

try {

redisTemplate.opsForHash().putAll(key, map);

if (time > 0) {

expire(key, time);

}

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 向一张hash表中放入数据,如果不存在将创建

*

* @param key 键

* @param item 项

* @param value 值

* @return true 成功 false失败

*/

public boolean hset(String key, String item, Object value) {

try {

redisTemplate.opsForHash().put(key, item, value);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 向一张hash表中放入数据,如果不存在将创建

*

* @param key 键

* @param item 项

* @param value 值

* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间

* @return true 成功 false失败

*/

public boolean hset(String key, String item, Object value, long time) {

try {

redisTemplate.opsForHash().put(key, item, value);

if (time > 0) {

expire(key, time);

}

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 删除hash表中的值

*

* @param key 键 不能为null

* @param item 项 可以使多个 不能为null

*/

public void hdel(String key, Object... item) {

redisTemplate.opsForHash().delete(key, item);

}

/**

* 判断hash表中是否有该项的值

*

* @param key 键 不能为null

* @param item 项 不能为null

* @return true 存在 false不存在

*/

public boolean hHasKey(String key, String item) {

return redisTemplate.opsForHash().hasKey(key, item);

}

/**

* hash递增 如果不存在,就会创建一个 并把新增后的值返回

*

* @param key 键

* @param item 项

* @param by 要增加几(大于0)

*/

public double hincr(String key, String item, double by) {

return redisTemplate.opsForHash().increment(key, item, by);

}

/**

* hash递减

*

* @param key 键

* @param item 项

* @param by 要减少记(小于0)

*/

public double hdecr(String key, String item, double by) {

return redisTemplate.opsForHash().increment(key, item, -by);

}

// ============================set=============================

/**

* 根据key获取Set中的所有值

* @param key 键

*/

public Set sGet(String key) {

try {

return redisTemplate.opsForSet().members(key);

} catch (Exception e) {

e.printStackTrace();

return null;

}

}

/**

* 根据value从一个set中查询,是否存在

*

* @param key 键

* @param value 值

* @return true 存在 false不存在

*/

public boolean sHasKey(String key, Object value) {

try {

return redisTemplate.opsForSet().isMember(key, value);

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 将数据放入set缓存

*

* @param key 键

* @param values 值 可以是多个

* @return 成功个数

*/

public long sSet(String key, Object... values) {

try {

return redisTemplate.opsForSet().add(key, values);

} catch (Exception e) {

e.printStackTrace();

return 0;

}

}

/**

* 将set数据放入缓存

*

* @param key 键

* @param time 时间(秒)

* @param values 值 可以是多个

* @return 成功个数

*/

public long sSetAndTime(String key, long time, Object... values) {

try {

Long count = redisTemplate.opsForSet().add(key, values);

if (time > 0)

expire(key, time);

return count;

} catch (Exception e) {

e.printStackTrace();

return 0;

}

}

/**

* 获取set缓存的长度

*

* @param key 键

*/

public long sGetSetSize(String key) {

try {

return redisTemplate.opsForSet().size(key);

} catch (Exception e) {

e.printStackTrace();

return 0;

}

}

/**

* 移除值为value的

*

* @param key 键

* @param values 值 可以是多个

* @return 移除的个数

*/

public long setRemove(String key, Object... values) {

try {

Long count = redisTemplate.opsForSet().remove(key, values);

return count;

} catch (Exception e) {

e.printStackTrace();

return 0;

}

}

// ===============================list=================================

/**

* 获取list缓存的内容

*

* @param key 键

* @param start 开始

* @param end 结束 0 到 -1代表所有值

*/

public List lGet(String key, long start, long end) {

try {

return redisTemplate.opsForList().range(key, start, end);

} catch (Exception e) {

e.printStackTrace();

return null;

}

}

/**

* 获取list缓存的长度

*

* @param key 键

*/

public long lGetListSize(String key) {

try {

return redisTemplate.opsForList().size(key);

} catch (Exception e) {

e.printStackTrace();

return 0;

}

}

/**

* 通过索引 获取list中的值

*

* @param key 键

* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推

*/

public Object lGetIndex(String key, long index) {

try {

return redisTemplate.opsForList().index(key, index);

} catch (Exception e) {

e.printStackTrace();

return null;

}

}

/**

* 将list放入缓存

*

* @param key 键

* @param value 值

*/

public boolean lSet(String key, Object value) {

try {

redisTemplate.opsForList().rightPush(key, value);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 将list放入缓存

* @param key 键

* @param value 值

* @param time 时间(秒)

*/

public boolean lSet(String key, Object value, long time) {

try {

redisTemplate.opsForList().rightPush(key, value);

if (time > 0)

expire(key, time);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 将list放入缓存

*

* @param key 键

* @param value 值

* @return

*/

public boolean lSet(String key, List value) {

try {

redisTemplate.opsForList().rightPushAll(key, value);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 将list放入缓存

*

* @param key 键

* @param value 值

* @param time 时间(秒)

* @return

*/

public boolean lSet(String key, List value, long time) {

try {

redisTemplate.opsForList().rightPushAll(key, value);

if (time > 0)

expire(key, time);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 根据索引修改list中的某条数据

*

* @param key 键

* @param index 索引

* @param value 值

* @return

*/

public boolean lUpdateIndex(String key, long index, Object value) {

try {

redisTemplate.opsForList().set(key, index, value);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

/**

* 移除N个值为value

*

* @param key 键

* @param count 移除多少个

* @param value 值

* @return 移除的个数

*/

public long lRemove(String key, long count, Object value) {

try {

Long remove = redisTemplate.opsForList().remove(key, count, value);

return remove;

} catch (Exception e) {

e.printStackTrace();

return 0;

}

}

}

4、创建rabbitmq的配置类

@Configuration

public class TopicConfig {

@Bean

public TopicExchange topicExchange() {

return new TopicExchange("seckill_topic", true, false);

}

@Bean

public Queue seckillQueue() {

return new Queue("seckillQueue", true);

}

@Bean

public Binding binding() {

return BindingBuilder.bind(seckillQueue()).to(topicExchange()).with("seckill.#");

}

}

5、创建数据库表

用户信息 

商品信息 

秒杀信息

订单信息

秒杀订单 

6、具体实现过程

用户登录 将要秒杀的商品数量预存在redis中 接收前端传回来的用户id,被秒杀商品id 验证用户是否为当前用户,是则进行下一步 获取被秒杀的商品信息 判断用户是否成功秒杀过商品,如果秒杀过,则结束进程,否则下一步 判断被秒杀商品是否还有库存,有,下一步,否则结束进程 将用户id和被秒杀商品信息传入队列进行处理 队列处理完成后,返回结果。

@ApiModelProperty(value = "秒杀功能")

@PostMapping("doseckill")

public Result doSeckill(@RequestParam Long userid, @RequestParam Long goodsid) {

//判断是否当前用户

RUser user = (RUser) redisUtil.get("user" + userid + ":");

if (user == null) {

return Result.fail("请登录!");

}

//缓存被秒杀商品的信息

if (redisUtil.get("g" + goodsid + ":") == null) {

GoodsVo goods = goodsService.selectGoodsById(goodsid);

redisUtil.set("g" + goodsid + ":", goods);

}

GoodsVo goods = (GoodsVo) redisUtil.get("g" + goodsid + ":");

//判断是否重复操作,已经成功秒杀,不能再次秒杀

// SeckillOrders seckillOrders = seckillOrdersService.getOne(new QueryWrapper()

// .lambda().eq(SeckillOrders::getUserId, userid)

// .eq(SeckillOrders::getGoodId, goods.getId()));

Object o = redisUtil.get("order:" + userid + goodsid);

if (o != null) {

return Result.fail("用户" + userid + ":每个用户只能秒杀一次.....");

}

//库存预减

Long count = redisUtil.decr("seckill" + goodsid + ":", 1);

if (count < 0) {

redisUtil.incr("seckill" + goodsid + ":", 1);

return Result.fail("用户" + userid + ":存库不足,秒杀失败.....");

}

//将秒杀请求添加到队列中

MqMessage message = new MqMessage(userid, goods);

String msg = JSON.toJSONString(message);

product.sendSeckillMessage(msg);

return Result.succ("秒杀中............");

}

用户登录

校验账号和密码是否正确,两者都正确则登陆成功,否则登陆失败。如果登录成功,就将用户的信息缓存在redis中,后续如果需要使用到用户的信息,直接从redis中获取即可,不用再次访问数据库。

public RUser Login(UserDto userDto) {

RUser user = new RUser();

BeanUtils.copyProperties(userDto,user);

QueryWrapper qw = new QueryWrapper<>();

qw.lambda().eq(RUser::getId,user.getId())

.eq(RUser::getPassword,user.getPassword());

RUser user1 =baseMapper.selectOne(qw);

if(user1!=null){

redisUtil.set("user"+user1.getId()+":",user1);

System.out.println(redisUtil.set("user"+user1.getId()+":",user1));

}

return user1;

}

秒杀功能的流程是:获得秒杀资格后,需要进行商品数量减少、生成订单、生成秒杀订单的操作,当三者都成功运行后,才能秒杀成功。

秒杀商品数量初始化

在系统初始化时,调用这个方法,将秒杀商品的数量存储在redis中,后续使用redis进行预减功能

@Override

    public void getSeckillCount() {

        List list = baseMapper.selectList(null);

        list.forEach(System.out::println);

        if (list.size()>0) {

            list.forEach(seckill -> {

                redisUtil.set("seckill" + seckill.getGoodsId() + ":", seckill.getStockCount());

                Object o = redisUtil.get("seckill" + seckill.getGoodsId() + ":");

                System.out.println(o.toString());

            });

        }

}

rabbitmq队列

生产者代码

把用户id和秒杀商品信息分发到队列中

public void sendSeckillMessage(String msg){

System.out.println("发送秒杀信息"+msg);

rabbitTemplate.convertAndSend("seckill_topic","seckill.message",msg);

}

消费者代码

从队列中获取用户id和商品信息。然后,判断该商品是否还有库存,是否已经秒杀过,如果都没有,则进行订单生产业务。

@RabbitListener(queues = "seckillQueue")

public void receive(String msg) {

MqMessage message = JSONObject.parseObject(msg, MqMessage.class);

Long userid = message.getUserid();

GoodsVo goods = message.getGoodsVo();

//判断是否还有库存

int stock = goods.getStockCount();

if(stock <= 0) {

return;

}

//判断是否已经秒杀到了

Orders order = ordersService.getOne(new QueryWrapper()

.lambda().eq(Orders::getUserId,userid)

.eq(Orders::getGoodsId, goods.getId()));

if(order != null) {

return;

}

ordersService.seckill(userid,goods);

}

订单模块

订单模块:将用户购买的商品的数量减少1,然后生成订单和秒杀订单。三者都成功后,在redis中存储用户id和订单id,作为秒杀成功的记录,如果用户再次进行秒杀时,直接从redis查询是否存在秒杀成功的记录,存在即返回已经秒杀

public void seckill(Long userid, GoodsVo goods) {

Seckill seckill = seckillMapper.selectOne(

new QueryWrapper().lambda().eq(Seckill::getGoodsId,goods.getId()));

seckill.setStockCount(seckill.getStockCount()-1);

seckillMapper.updateById(seckill);

//生成订单

Orders orders = new Orders();

orders.setUserId(userid).setGoodsId(goods.getId())

.setGoodsCount(1).setStatu(0).setCreateDate(new Date())

.setGoodsPrice(seckill.getSeckillPrice());

baseMapper.insert(orders);

//生成秒杀订单

SeckillOrders seckillOrders = new SeckillOrders();

seckillOrders.setOrderId(orders.getId());

seckillOrders.setUserId(userid);

seckillOrders.setGoodId(goods.getId());

// 保存秒杀订单信息

int i=seckillOrdersMapper.insert(seckillOrders);

redisUtil.set("order:"+userid+goods.getId(),i);

}

注意事项:

性能优化:在实际生产环境中,您可能需要考虑使用Redis的Lua脚本来确保库存扣减的原子性,并考虑使用分布式锁来避免超卖。限流:在秒杀场景下,大量的请求可能会导致系统崩溃。因此,您可能需要在应用层或网络层实现限流策略。异步处理:使用RabbitMQ进行异步处理可以确保秒杀接口的快速响应,并避免同步处理订单逻辑导致的性能瓶颈。持久化:虽然上述案例中没有明确提到,但您应该确保将秒杀成功的订单信息持久化到数据库中,以便后续的处理和查询。错误处理:在分布式系统中,错误处理是非常重要的。您应该确保在RabbitMQ消费者中正确处理各种异常情况,并考虑使用重试机制来确保消息的可靠传递。

 

精彩内容

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。

发表评论

返回顶部暗黑模式