订单号的生成,这是每个交易系统都碰到的问题。方案有很多种,比如uuid、时间戳+随机数、数据库自增长等等。这些方案或多或少都存在一些问题。比如uuid索引性能低下,时间戳+随机数存在可能重复的问题,数据库自增长如果是分布式多表的情况显然是不合适的。更蛋疼的是笔者所在公司业务方提出蛋疼的要求:订单号的格式为yyyymmdd+7位有序数字,比如当天第一笔为yyyymmdd0000001,第二笔就是yyyymmdd0000002,以此类推。公司不怕从订单号看到订单的业务信息,就是这么牛逼,不服?Ok,我们来聊聊当时的解决方案。
1.转变思路
上文的订单生产方案可以看到有一个共同的特点:在使用时生产!!!我们知道,计算机的任何行为都会有一定资源开销。来分析一下订单号的格式为yyyymmdd+7位有序数字的性能问题:如果是传统的做法,就是查询订单号当日的最大值+1作为当前的订单号,因为查询最大值牵涉到数据库记录排序的问题,这就是一个很大的性能开销,况且如果是多库多表的情况就更糟糕了。
改变思路,对,改变思路,因为上面所例举的方法都是在创建订单时即时生成的有序订单号,但可以在生成订单之前提前生成订单号,那么就可以减少这部分的开销了。
2.引入redis
1)创建定时任务,每天凌晨4点生成次日订单号,由于凌晨4点交易量一般不会高,为了减少对主业务的影响故选此时间段。
2)所有生成的订单号放入一个双向队列里Deque,队列Deque保存在redis的<key,value>结构中,其中key为yyyymmdd+7位有序数字,value为双向队列Deque。
3)订单系统获取订单号,订单系统从redis中的Deque队列获取首元素作为订单号。由于使用pollFirst()方法,首元素会即时删除,再加上redis是单线程,订单系统基本上不会获取到相同的订单号,即使订单系统是分布式服务也不会有问题。
4)为了防止出现订单不够用的情况,定时任务系统会定时检测Deque队列的长度,如果订单号过少,在队列Deque尾部追加一定数量的订单号,当然,以当前尾元素为起点进行追加。
5)为了节省redis内存空间,每天凌晨3点删除保存保存前一天订单号的Deque队列。
定时任务系统、订单系统、redis关系结构图如下:
由于牵涉到公司的一些保密协议,代码就不便提供了,请谅解。
总结
使用redis第三方组件让我们的订单生成得到了几大优化:
1)杜绝了订单号冲突的问题,以前经常出现这种情况,在同一时间段下单时,查到同一个订单号最大值,产生订单号冲突,导致交易失败,影响用户体验。
2)减少了数据库消耗。由于订单号的生成在redis上操作完成,和数据库基本没啥关系,因此哪些繁琐的查询查询、恶心的数据库排序通通见鬼去吧。间接也提高了系统性能。
最后,笔者想多说一句,一个问题可能有多种解决方案,当一个方案有瑕疵时,我们何不换一种思路呢?