摘要:

本篇博文是“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 ,仔细观察控制台的输出信息,会看一些令自己躁动不安的东西:

竟然会出现“重复生成了重复的订单编号”!而且,打开数据库表进行观察,会发现“他娘的1000个线程生成订单编号,竟然只有900多个记录”,这就说明了这么多个线程在执行生成订单编号的逻辑期间出现了“重复的订单编号”!如下图所示:

因此,此种基于随机数生成唯一ID或者订单编号的方式,我们是可以Pass掉了(当然啦,在并发量不是很高的情况下,这种方式还是阔以使用的,因为简单而且易于理解啊!),鉴于此种“基于随机数生成”的方式在高并发的场景下并不符合我们的要求,接下来,我们将介绍另外一种比较流行的、典型的方式,即“分布式唯一ID生成算法-雪花算法”来实现。

对于“雪花算法”的介绍,各位小伙伴可以参考Github上的这一链接,我觉得讲得还是挺清晰的:https://github.com/souyunku/SnowFlake ,详细的Debug在这里就不赘述了,下面截取了部分概述:

SnowFlake算法在分布式的环境下,之所以能高效率的生成唯一的ID,我觉得其中很重要的一点在于其底层的实现是通过“位运算”来实现的,简单来讲,就是直接跟机器打交道!其底层数据的存储结构(64位)如下图所示:

下面,我们就直接基于雪花算法来生成秒杀系统中需要的订单编号吧!

(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;}@Overridepublic 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;}//产生下一个IDpublic 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 {//不同毫秒内,序列号置为0sequence = 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个对应的订单编号,如下图所示:

除此之外,各位小伙伴还可以将线程数从1000调整为10000、100000甚至1000000,然后观察控制台的输出信息以及数据库表的记录等等。

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

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

补充:

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

2、实战期间有任何问题都可以留言或者与Debug联系、交流;QQ技术交流群:605610429,顺便关注一下Debug的技术微信公众号呗:

转载于:https://www.cnblogs.com/SteadyJack/p/11232544.html

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

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

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...

  2. asp按时间自动递增编号_Java秒杀系统实战系列-分布式唯一ID生成订单编号

    本文是"Java秒杀系统实战系列文章"的第七篇,在本文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们将介绍两种方法 ...

  3. Java秒杀系统实战系列~基于Redisson的分布式锁优化秒杀逻辑

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十五篇,本文我们将借助综合中间件Redisson优化"秒杀系统中秒杀的核心业务逻辑",解决Redis的原子 ...

  4. Java秒杀系统实战系列~商品秒杀代码实战

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第六篇,本篇博文我们将进入整个秒杀系统核心功能模块的代码开发,即"商品秒杀"功能模块的代码实战. 内容: & ...

  5. Java秒杀系统实战系列~JMeter压力测试重现秒杀场景中超卖等问题

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十二篇,本篇博文我们将借助压力测试工具Jmeter重现秒杀场景(高并发场景)下出现的各种典型的问题,其中最为经典的当属&quo ...

  6. Java秒杀系统实战系列~RabbitMQ死信队列处理超时未支付的订单(转)

    转自: https://juejin.cn/post/6844903903130042376 文末有源代码,非常棒 摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十篇,本篇 ...

  7. Java秒杀系统实战系列~构建SpringBoot多模块项目

    摘要:本篇博文是"Java秒杀系统实战系列文章"的第二篇,主要分享介绍如何采用IDEA,基于SpringBoot+SpringMVC+Mybatis+分布式中间件构建一个多模块的项 ...

  8. Java秒杀系统实战系列~数据库级别Sql的优化与代码的调整

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十三篇,从本篇文章开始我们将进入"秒杀代码优化"环节,本文将首先从数据库级别Sql的优化入手,结合调整秒杀 ...

  9. Java秒杀系统实战系列~定时任务补充处理超时未支付的订单

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十一篇,本篇博文我们将借助定时任务调度组件来辅助"失效超时未支付的订单记录"的处理,用以解决上篇博文中采用 ...

最新文章

  1. java -- 线程的生命周期
  2. 最重要的会计期间是_非会计专业考生如何备考注会?难度多大?
  3. joc杂志影响因子2019_另类统计!2019影响因子贡献文章排行榜,看完有点心塞
  4. QQ2009任务栏的QQ图标怎么隐藏
  5. NDK建立多个共享库
  6. 关于Tomcat双击startup.bat文件一闪而过问题
  7. 图解TCPOP-SMTP
  8. drupal ajax json异步调用
  9. 聊天机器人5步重塑酒店业
  10. FCPX内置音效库汉化版
  11. Chrome 启动页面被114篡改的修复
  12. ctf web3 30 flag就在这里快来找找吧http://123.206.87.240:8002/web3
  13. GlusterFS元数据机制分析
  14. 看名言后的心得体会学会融会贯通
  15. Windows和Ubuntu搭建局域网中共享文件夹
  16. 汉字转拼音,用户表增加拼音字段,并将汉字姓名对应的拼音赋值给拼音字段
  17. 查询速度至少为160MHz的PC的制造商
  18. C#NetFrame3.5 JsonHelper
  19. CJSON 创建含多层嵌套结构体及结构体解析
  20. MySQL 的安装

热门文章

  1. 【codevs1380】没有上司的舞会
  2. mysql锁问题排查_Mysql死锁问题如何排查和解决?
  3. android按钮切换颜色,togglebutton
  4. OpenGL基础49:高度贴图(下)
  5. OpenGL基础20:镜面光照
  6. Wannafly挑战赛28: B. msc和mcc(思维)
  7. bzoj 3381: [Usaco2004 Open]Cave Cows 2 洞穴里的牛之二(RMQ)
  8. python中random库
  9. kubernetes kubelet参数
  10. java实现tcp服务器(单线程、多线程)、客户端