Java秒杀系统实战系列~分布式唯一ID生成订单编号

简介: 摘要:本篇博文是“Java秒杀系统实战系列文章”的第七篇,在本博文中我们将重点介绍 “在高并发,如秒杀的业务场景下如何生成全局唯一、趋势递增的订单编号”,我们将介绍两种方法,一种是传统的采用随机数生成的方式,另外一种是采用当前比较流行的“分布式唯一ID生成算法-雪花算法”来实现。

摘要:

本篇博文是“Java秒杀系统实战系列文章”的第七篇,在本博文中我们将重点介绍 “在高并发,如秒杀的业务场景下如何生成全局唯一、趋势递增的订单编号”,我们将介绍两种方法,一种是传统的采用随机数生成的方式,另外一种是采用当前比较流行的“分布式唯一ID生成算法-雪花算法”来实现。

内容:

在上一篇博文,我们完成了商品秒杀业务逻辑的代码实战,在该代码中,我们还实现了“当用户秒杀成功后,需要在数据库表中为其生成一笔秒杀成功的订单记录”的功能,其对应的代码如下所示:

private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{
    //TODO:记录抢购成功后生成的秒杀订单记录
 
    ItemKillSuccess entity=new ItemKillSuccess();
    
    //此处为订单编号的生成逻辑
    String orderNo=String.valueOf(snowFlake.nextId());
    //entity.setCode(RandomUtil.generateOrderCode());   //传统时间戳+N位随机数
    entity.setCode(orderNo); //雪花算法
 
    entity.setItemId(kill.getItemId());
    entity.setKillId(kill.getId());
    entity.setUserId(userId.toString());
    entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue());
    entity.setCreateTime(DateTime.now().toDate());
    //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法
    if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
        int res=itemKillSuccessMapper.insertSelective(entity);
 
        //其他逻辑省略
    }
}

在该实现逻辑中,其核心要点在于“在高并发的环境下,如何高效的生成订单编号”,那么如何才算是高效呢?Debug认为应该满足以下两点:

(1)保证订单编号的生成逻辑要快、稳定,减少时延

(2)要保证生成的订单编号全局唯一、不重复、趋势递增、有时序性

下面,我们采用两种方式来生成“订单编号”,并自己写一个多线程的程序模拟生成的订单编号是否满足条件。

值得一提的是,为了能直观的观察多线程并发生成的订单编号是否具有唯一性、趋势递增,在这里Debug借助了一张数据库表 random_code 来存储生成的订单编号,其DDL如下所示:

CREATE TABLE `random_code` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_code` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

从该数据库表数据结构定义语句中可以看出,我们设定了 订单编号字段code 为唯一!所以如果高并发多线程生成的订单编号出现重复,那么在插入数据库表的时候必然会出现错误

下面,首先开始我们的第一种方式吧:基于随机数的方式生成订单编号

(1)首先是建立一个Thread类,其run方法的执行逻辑为生成订单编号,并将生成的订单编号插入数据库表中,其代码如下所示:

/**
 * 随机数生成的方式-Thread
 * @Author:debug (SteadyJack)
 * @Date: 2019/7/11 10:30
 **/
public class CodeGenerateThread implements Runnable{
 
    private RandomCodeMapper randomCodeMapper;
 
    public CodeGenerateThread(RandomCodeMapper randomCodeMapper) {
        this.randomCodeMapper = randomCodeMapper;
    }
 
    @Override
public void run() {
    //生成订单编号并插入数据库
        RandomCode entity=new RandomCode();
        entity.setCode(RandomUtil.generateOrderCode());
        randomCodeMapper.insertSelective(entity);
    }
}

其中,RandomUtil.generateOrderCode()的生成逻辑是借助ThreadLocalRandom来实现的,其完整的源代码如下所示:

/**
 * 随机数生成util
 * @Author:debug (SteadyJack)
 * @Date: 2019/6/20 21:05
 **/
public class RandomUtil {
    private static final SimpleDateFormat dateFormatOne=new SimpleDateFormat("yyyyMMddHHmmssSS");
 
    private static final ThreadLocalRandom random=ThreadLocalRandom.current();
    //生成订单编号-方式一
    public static String generateOrderCode(){
        //TODO:时间戳+N为随机数流水号
        return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4);
    }
 
    //N为随机数流水号
    public static String generateNumber(final int num){
        StringBuffer sb=new StringBuffer();
        for (int i=1;i<=num;i++){
            sb.append(random.nextInt(9));
        }
        return sb.toString();
    }
}

(2)紧接着是在 BaseController控制器 中开发一个请求方法,目的正是用来模拟前端高并发触发产生多线程并生成订单编号的逻辑,在这里我们暂且用1000个线程进行模拟,其源代码如下所示:

@Autowired
private RandomCodeMapper randomCodeMapper;
 
//测试在高并发下多线程生成订单编号-传统的随机数生成方法
@RequestMapping(value = "/code/generate/thread",method = RequestMethod.GET)
public BaseResponse codeThread(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<1000;i++){
            executorService.execute(new CodeGenerateThread(randomCodeMapper));
        }
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}

(3)完了之后,就可以将整个项目、系统运行在外置的tomcat中了,然后打开postman,发起一个Http的Get请求,请求链接为:http://127.0.0.1:8092/kill/base/code/generate/thread ,仔细观察控制台的输出信息,会看一些令自己躁动不安的东西:
image
竟然会出现“重复生成了重复的订单编号”!而且,打开数据库表进行观察,会发现“他娘的1000个线程生成订单编号,竟然只有900多个记录”,这就说明了这么多个线程在执行生成订单编号的逻辑期间出现了“重复的订单编号”!如下图所示:
image
因此,此种基于随机数生成唯一ID或者订单编号的方式,我们是可以Pass掉了(当然啦,在并发量不是很高的情况下,这种方式还是阔以使用的,因为简单而且易于理解啊!)

鉴于此种“基于随机数生成”的方式在高并发的场景下并不符合我们的要求,接下来,我们将介绍另外一种比较流行的、典型的方式,即“分布式唯一ID生成算法-雪花算法”来实现。

对于“雪花算法”的介绍,各位小伙伴可以参考Github上的这一链接,我觉得讲得还是挺清晰的:https://github.com/souyunku/SnowFlake ,详细的Debug在这里就不赘述了,下面截取了部分概述:
image
SnowFlake算法在分布式的环境下,之所以能高效率的生成唯一的ID,我觉得其中很重要的一点在于其底层的实现是通过“位运算”来实现的,简单来讲,就是直接跟机器打交道!其底层数据的存储结构(64位)如下图所示:
image
下面,我们就直接基于雪花算法来生成秒杀系统中需要的订单编号吧!

(1)同样的道理,我们首先定义一个Thread类,其run方法的实现逻辑是借助雪花算法生成订单编号并将其插入到数据库中。

/** 基于雪花算法生成全局唯一的订单编号并插入数据库表中
 * @Author:debug (SteadyJack)
 * @Date: 2019/7/11 10:30
 **/
public class CodeGenerateSnowThread implements Runnable{
 
    private static final SnowFlake SNOW_FLAKE=new SnowFlake(2,3);
 
    private RandomCodeMapper randomCodeMapper;
 
    public CodeGenerateSnowThread(RandomCodeMapper randomCodeMapper) {
        this.randomCodeMapper = randomCodeMapper;
    }
 
    @Override
    public void run() {
        RandomCode entity=new RandomCode();
        //采用雪花算法生成订单编号
        entity.setCode(String.valueOf(SNOW_FLAKE.nextId()));
        randomCodeMapper.insertSelective(entity);
    }
}

其中,SNOW_FLAKE.nextId() 的方法正是采用雪花算法生成全局唯一的订单编号的逻辑,其完整的源代码如下所示:

/** * 雪花算法
 * @author: zhonglinsen
 * @date: 2019/5/20
 */
public class SnowFlake {
    //起始的时间戳
    private final static long START_STAMP = 1480166465631L;
 
    //每一部分占用的位数
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5;//数据中心占用的位数
 
    //每一部分的最大值
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
 
    //每一部分向左的位移
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
 
    private long dataCenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStamp = -1L;//上一次时间戳
 
    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_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
    public synchronized long nextId() {
        long currStamp = getNewStamp();
        if (currStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
 
        if (currStamp == lastStamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStamp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }
 
        lastStamp = currStamp;
 
        return (currStamp - START_STAMP) << TIMESTAMP_LEFT //时间戳部分
                | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }
 
    private long getNextMill() {
        long mill = getNewStamp();
        while (mill <= lastStamp) {
            mill = getNewStamp();
        }
        return mill;
    }
 
    private long getNewStamp() {
        return System.currentTimeMillis();
    }
}

(2)紧接着,我们在BaseController中开发一个请求方法,用于模拟前端触发高并发产生多线程抢单的场景。

/**
 * 测试在高并发下多线程生成订单编号-雪花算法
 * @return
 */
@RequestMapping(value = "/code/generate/thread/snow",method = RequestMethod.GET)
public BaseResponse codeThreadSnowFlake(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<1000;i++){
            executorService.execute(new CodeGenerateSnowThread(randomCodeMapper));
        }
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}

(3)完了之后,我们采用Postman发起一个Http的Get请求,其请求链接如下所示:http://127.0.0.1:8092/kill/base/code/generate/thread/snow ,观察控制台的输出信息,可以看到“一片安然的景象”,再观察数据库表的记录,可以发现,1000个线程成功触发生成了1000个对应的订单编号,如下图所示:
image
除此之外,各位小伙伴还可以将线程数从1000调整为10000、100000甚至1000000,然后观察控制台的输出信息以及数据库表的记录等等。

Debug亲测了1w跟10w的场景下是木有问题的,100w的线程数的测试就交给各位小伙伴去试试了(时间比较长,要有心理准备哦!)至此,我们就可以将雪花算法生成全局唯一的订单编号的逻辑应用到我们的“秒杀处理逻辑”中,即其代码(在KillService的commonRecordKillSuccessInfo方法中)如下所示:

ItemKillSuccess entity=new ItemKillSuccess();
String orderNo=String.valueOf(snowFlake.nextId());//雪花算法
entity.setCode(orderNo); 
//其他代码省略

补充:
1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版)

2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill

相关文章
|
2月前
|
数据管理 API 调度
鸿蒙HarmonyOS应用开发 | 探索 HarmonyOS Next-从开发到实战掌握 HarmonyOS Next 的分布式能力
HarmonyOS Next 是华为新一代操作系统,专注于分布式技术的深度应用与生态融合。本文通过技术特点、应用场景及实战案例,全面解析其核心技术架构与开发流程。重点介绍分布式软总线2.0、数据管理、任务调度等升级特性,并提供基于 ArkTS 的原生开发支持。通过开发跨设备协同音乐播放应用,展示分布式能力的实际应用,涵盖项目配置、主界面设计、分布式服务实现及部署调试步骤。此外,深入分析分布式数据同步原理、任务调度优化及常见问题解决方案,帮助开发者掌握 HarmonyOS Next 的核心技术和实战技巧。
255 76
鸿蒙HarmonyOS应用开发 | 探索 HarmonyOS Next-从开发到实战掌握 HarmonyOS Next 的分布式能力
|
3天前
|
人工智能 Kubernetes 异构计算
大道至简-基于ACK的Deepseek满血版分布式推理部署实战
本教程演示如何在ACK中多机分布式部署DeepSeek R1满血版。
|
26天前
|
存储 缓存 Java
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
39 9
|
2月前
|
物联网 调度 vr&ar
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
鸿蒙技术分享:HarmonyOS Next 深度解析 随着万物互联时代的到来,华为发布的 HarmonyOS Next 在技术架构和生态体验上实现了重大升级。本文从技术架构、生态优势和开发实践三方面深入探讨其特点,并通过跨设备笔记应用实战案例,展示其强大的分布式能力和多设备协作功能。核心亮点包括新一代微内核架构、统一开发语言 ArkTS 和多模态交互支持。开发者可借助 DevEco Studio 4.0 快速上手,体验高效、灵活的开发过程。 239个字符
236 13
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
|
2月前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
83 10
|
2月前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
3天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
36 14
|
6天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
7天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
1月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
109 17