SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
  TEZNKK3IfmPf 2023年11月13日 23 0

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 的并发量
SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
商品库存只有10个,然后查看生成的订单,订单号未重复
SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
然后 postman 查询未下单成功的订单
SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
再查询一下 下单成功的订单
SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
查询到未支付状态的订单 ,前端再去调用支付代码。

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:纳秒

本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月13日 0

暂无评论

推荐阅读
  TEZNKK3IfmPf   24天前   51   0   0 java
  TEZNKK3IfmPf   2024年05月31日   55   0   0 java
TEZNKK3IfmPf