SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发。
SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发
1 项目准备
SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】本文章 基于此
本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。
订单系统,用户下单,即要保存即时性,也要保证流畅性,同时还要防止超卖,本文章是基于 RabbitMQ 消息队列 + Redis 实现的下单,当然后续还会的秒杀系统设计 以及后续的微服务以及熔断控制等等。
在目前高并发分布式情境下,生成唯一标识(如这里的订单 sn)是重中之重,目前业界也有很多算法可以实现,比较有名的就是雪花算法(SnowFlake)!!!
1 分布式系统中雪花算法优化
首先在配置文件 application.yml 添加添加 workId 与 datacenterId
#开发环境配置
server:
workId: 2
datacenterId: 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;
}
}
2 雪花算法 SnowFlake
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,结果是31
private 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 {
//不同毫秒内,序列号置为0
sequence = 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 + "秒");
}
}
3 商品下单时 生成订单号
基本实现思路是 用户下单,有基本库存时,生成订单号,发送队列消息,将生成的订单号返回到前端
SnowFlakeCompone snowFlakeCompone;
private OrderMQSender mqSender;
public 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 {
@Autowired
private OrderService orderService;
@Autowired
private 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-11-snow_flake
有兴趣可以关注一下公众号:biglead
- 创建SpringBoot基础项目
- SpringBoot项目集成mybatis
- SpringBoot 集成 Druid 数据源【SpringBoot系列3】
- SpringBoot MyBatis 实现分页查询数据【SpringBoot系列4】
- SpringBoot MyBatis-Plus 集成 【SpringBoot系列5】
- SpringBoot mybatis-plus-generator 代码生成器 【SpringBoot系列6】
- SpringBoot MyBatis-Plus 分页查询 【SpringBoot系列7】
- SpringBoot 集成Redis缓存 以及实现基本的数据缓存【SpringBoot系列8】
- SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】
- SpringBoot Security认证 Redis缓存用户信息【SpringBoot系列10】
11 . SpringBoot 整合 RabbitMQ 消息队列【SpringBoot系列11】
12 . SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】
更多推荐
所有评论(0)