SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发
SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】本文章 基于此
本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。
订单系统,用户下单,即要保存即时性,也要保证流畅性,同时还要防止超卖,本文章是基于 RabbitMQ 消息队列 + Redis 实现的下单,当然后续还会的秒杀系统设计 以及后续的微服务以及熔断控制等等。
在目前高并发分布式情境下,生成唯一标识(如这里的订单 sn)是重中之重,目前业界也有很多算法可以实现,比较有名的就是雪花算法(SnowFlake)!!!
首先在配置文件 application.yml 添加添加 workId 与 datacenterId
#开发环境配置
server:workId: 2datacenterId: 5
微服务下 最好用bootstrap.yml 而不是 application.yml 原因是因为优先级高,防止被覆盖或者无法生效 。
在分布式系统,不同服务器使用不同workId,datacenterId。
然后在微服务启动的时候,workId和datacenterId作为参数传入,来做为 雪花算法 数据标识Id与 机器标识ID
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class SnowFlakeCompone {@Value("${server.workId}")private long workId;@Value("${server.datacenterId}")private long datacenterId;private static volatile SnowFlake instance;/*** 获取实例* @return*/public SnowFlake getInstance(){if(instance == null){synchronized (SnowFlake.class){if(instance == null){instance = new SnowFlake(workId, datacenterId);}}}return instance;}
}
public class SnowFlake {/*** 起始的时间戳*/private final static long START_STMP = 1480166465631L;/*** 每一部分占用的位数*/private final static long SEQUENCE_BIT = 12; //序列号占用的位数private final static long MACHINE_BIT = 5; //机器标识占用的位数private final static long DATACENTER_BIT = 5;//数据中心占用的位数/*** 每一部分的最大值*///支持的最大数据标识id,结果是31private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);//支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);// 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);/*** 每一部分向左的位移*///机器ID向左移12位private final static long MACHINE_LEFT = SEQUENCE_BIT;//数据标识id向左移17位(12+5)private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;//时间截向左移22位(5+5+12)private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;private long datacenterId; //数据中心private long machineId; //机器标识private long sequence = 0L; //序列号private long lastStmp = -1L;//上一次时间戳/*** 构造函数* @param datacenterId 数据标识Id(0-31)* @param machineId //机器标识Id(0-31)*/public SnowFlake(long datacenterId, long machineId) {if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");}if (machineId > MAX_MACHINE_NUM || machineId < 0) {throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");}this.datacenterId = datacenterId;this.machineId = machineId;}/*** 产生下一个ID** @return*/public synchronized long nextId() {//获取当前时间戳long currStmp = getNewstmp();//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常if (currStmp < lastStmp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id");}//如果是同一时间生成的,则进行毫秒内序列递增if (currStmp == lastStmp) {//相同毫秒内,序列号自增sequence = (sequence + 1) & MAX_SEQUENCE;//同一毫秒的序列数已经达到最大if (sequence == 0L) {currStmp = getNextMill();}} else {//不同毫秒内,序列号置为0sequence = 0L;}lastStmp = currStmp;return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分| datacenterId << DATACENTER_LEFT //数据中心部分| machineId << MACHINE_LEFT //机器标识部分| sequence; //序列号部分}private long getNextMill() {long mill = getNewstmp();while (mill <= lastStmp) {mill = getNewstmp();}return mill;}private long getNewstmp() {return System.currentTimeMillis();}public static void main(String[] args) {SnowFlake snowFlake = new SnowFlake(2, 5);long start = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {System.out.println(snowFlake.nextId());}System.out.println((System.currentTimeMillis() - start)/1000 + "秒");}
}
基本实现思路是 用户下单,有基本库存时,生成订单号,发送队列消息,将生成的订单号返回到前端
@AutowiredSnowFlakeCompone snowFlakeCompone;@Autowiredprivate OrderMQSender mqSender;@Overridepublic R createPreOrder(Long goodsId, Long userId) {log.info("预下单处理 userId:{} goodsId:{} ", userId, goodsId);//获取redis中的商品库存 先判断商品是否有库存Boolean aBoolean = redisTemplate.hasKey("goodStock:" + goodsId);if (Boolean.FALSE.equals(aBoolean)) {return R.error("下单失败 商品库存不足");}//Redis 缓存获取商品库存int goodsStock = Integer.valueOf(redisTemplate.opsForValue().get("goodStock:" + goodsId).toString());if (goodsStock == 0) {return R.error("下单失败 商品库存不足");}//生成订单号long sn = snowFlakeCompone.getInstance().nextId(); //保存到redis中 状态 doing 正在处理中 //过期时间30分钟redisTemplate.opsForValue().set("sn:" + sn, "doing",30, TimeUnit.MINUTES);//发送下单消息SecKillMessage message = new SecKillMessage(userId, goodsId, sn);mqSender.sendCommonOrderMessage(JsonUtils.toJson(message));//把商品订单号返回到前端return R.okData(sn);}
然后 前端根据这个预下单的 订单号轮循查询订单详情,根据不同的状态码来实现不同的页面显示
@Api(tags="订单模块")
@RestController()
@RequestMapping("/orders")
@Slf4j
public class OrderController {@Autowiredprivate OrderService orderService;@Autowiredprivate RedisTemplate redisTemplate;/*** 查询订单状态与详情* 商品-下单入口调用* @param sn* @return*/@GetMapping("/statues/detail/{sn}")public R detailAndStatue(@PathVariable("sn") Long sn) {//redis 中查询状态Boolean aBoolean = redisTemplate.hasKey("sn:" + sn);if(Boolean.FALSE.equals(aBoolean)){return R.error("下单失败");}String snStatues = redisTemplate.opsForValue().get("sn:" +sn).toString();if(snStatues.equals("doing")){return R.error(202,"处理中");}if(!snStatues.equals("ok")){return R.error(203,snStatues);}//下单成功 返回订单信息OrderVo orderVo = orderService.detailFromSn(sn);return R.okData(orderVo);}}
然后启动项目 使用 apache-jmeter-5.5 调试 20000 的并发量

商品库存只有10个,然后查看生成的订单,订单号未重复

然后 postman 查询未下单成功的订单


再查询一下 下单成功的订单

查询到未支付状态的订单 ,前端再去调用支付代码。
redisTemplate.opsForValue().set("2","早起的年轻人",2, TimeUnit.SECONDS);//过期时间2秒
redisTemplate.opsForValue().set("2","早起的年轻人",2, TimeUnit.MINUTES);//过期时间2分钟
redisTemplate.opsForValue().set("2","早起的年轻人",2, TimeUnit.HOURS);//过期时间2小时
redisTemplate.opsForValue().set("2","早起的年轻人",2, TimeUnit.DAYS);//过期时间2天
时间类型:TimeUnit
TimeUnit.SECONDS:秒
TimeUnit.MINUTES:分
TimeUnit.HOURS:时
TimeUnit.DAYS:日
TimeUnit.MILLISECONDS:毫秒
TimeUnit.MILLISECONDS:微秒
TimeUnit.NANOSECONDS:纳秒
本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。
本文章只有核心代码,全部代码请查看对应源码
项目源码在这里 :https://gitee.com/android.long/spring-boot-study/tree/master/biglead-api-10-seckill
有兴趣可以关注一下公众号:biglead