当前位置:首页 > 问答 > 正文

用Redis搞自增值提升系统效率,重构里这样设置更方便也快不少

(引用来源:某电商平台订单号生成系统的重构实践)

之前我们那个系统,生成订单号是个麻烦事,最开始的想法挺直接的,就是用一个数据库表,里面就一个字段,专门用来存当前最大的订单号,每次有新订单要生成,就得先去这个表里,把那个最大的号码查出来,然后给它加一,再把这个新号码更新回数据库,最后才用这个新号码去生成完整的订单号,听起来好像没啥问题,对吧?但订单量一上去,比如做活动的时候,每秒几百上千单,这个法子就顶不住了,数据库那张表就成了个瓶颈,动不动就锁住,订单生成的速度慢得像蜗牛,有时候还会出现好几个订单拿到同一个号的情况,搞得一团糟。

后来我们琢磨着得换个法子,就想到了用Redis,Redis这东西,它所有的操作都在内存里完成,速度快得不是一星半点,它自己就带了一个命令叫INCR,这个命令是原子性的,啥叫原子性呢?简单说就是,这个操作是绝对排他的,不可能被打断,就算有一万个请求同时让Redis去INCR一个键,Redis也会排着队,一个一个来给这个键的值加一,而且保证每个请求拿到的数字都是唯一的,绝对不会重复,这不正好就是我们想要的效果吗?

说干就干,重构的时候,我们就把生成订单号核心的那一步,从去数据库查和更新,换成了直接调Redis的INCR命令,流程一下子变得特别简单:应用服务器接到生成订单的请求后,直接向Redis发一个命令,INCR order_id_seq,Redis瞬间就返回一个全新的、比上一个数字大1的整数值,然后应用服务器再用这个数字,按照我们的规则(比如加上日期、用户ID后缀什么的)拼装成最终的订单号,就这么一下,速度快了太多。

(引用来源:对比测试数据)

用Redis搞自增值提升系统效率,重构里这样设置更方便也快不少

我们做过对比,以前用数据库的方式,就算做了优化,每秒能成功生成的订单号峰值也就在几百个左右,而且数据库的CPU开销很大,换成Redis之后,在同样的硬件条件下,光说INCR这个操作,每秒处理几万次都轻轻松松,根本就不是一个数量级的,因为Redis是单线程处理命令,没有锁的竞争开销,内存操作又极快,所以响应时间非常稳定,几乎都在毫秒级别以下,这样一来,整个订单创建流程的瓶颈一下子就移走了,系统在处理高并发订单时变得非常顺畅。

(引用来源:实际应用中遇到的细节问题及解决方案)

刚开始用的时候也遇到点小插曲,我们担心过,万一Redis服务器重启了,这个自增的序列数字会不会丢?要是丢了,那不就可能产生重复的号码了吗?后来发现Redis是考虑了这点的,它虽然主要是内存数据库,但提供了持久化机制,我们可以配置一种叫AOF的持久化方式,让它把像INCR这样的写命令实时记到硬盘上,这样就算Redis重启,它也能根据日志把数据恢复回来,序列值就能接着上次的继续,不会乱,还有一种更简单的办法,就是定期手动执行一个命令,把当前序列值存到MySQL里做个备份,真要有问题,再从数据库里读回来设置到Redis里也行,不过我们最后选了AOF,觉得更省心。

用Redis搞自增值提升系统效率,重构里这样设置更方便也快不少

我们还用到了Redis的另一个命令叫INCRBY,这个INCR是每次加一,INCRBY可以指定一次加多少,比如有时候我们不是一条一条生成订单号,而是先批量申请一段号码回来,放在本地慢慢用,这样就可以用 INCRBY order_id_seq 1000,一次性把序列值增加1000,然后把当前返回的值到加上999这个范围之间的号码都划归这次申请,这样就能减少网络请求的次数,在特定场景下效率更高。

(引用来源:扩展至其他业务场景)

自从订单号生成这个事儿用Redis做顺了之后,我们发现这个思路能用的地方还挺多,比如说,搞活动的时候发优惠券,每张优惠券也需要一个唯一的券码,用Redis生成一个序列号作为核心部分,又快又好,还有用户发表帖子、评论的ID,也可以这么干,只要是系统里需要快速、绝对唯一地生成一个不断增长的数字序列的地方,几乎都可以套用这个模式,它把原来一个很重、很容易成为瓶颈的数据库操作,变成了一个轻量级、高性能的缓存层操作,效果是立竿见影的。

这次重构给我们的最大启发就是,像Redis这种专门工具,把它用在最适合它的场景里,哪怕只是替换掉一个看起来不大的环节,带来的效率提升和系统稳定性的增强,都是非常可观的,关键它用起来还特别简单,几行代码就搞定了原来一套复杂的逻辑,确实方便也快了不少。