作者:京东物流 张仲良

一、背景:

在古代物流的理论作业流程中,会有大量关系到经营相干信息的数据产生,如商家,车队,站点,分拣核心,客户等等相干的信息数据,这些数据间接撑持齐了物流的整个业务流转,具备非常重要的位置,那么对于这一类数据咱们须要提供根本的增删改查存的能力,目前京东物流的根底数据是由中台配运组来整体负责。

在根底数据的惯例能力当中,数据的存取是最根底也是最重要的能力,为了整体进步数据的读取能力,缓存技术在根底数据的场景中失去了宽泛的应用,上面会重点展现一下配运组近期针对数据缓存做的瘦身实际。

二、计划:

这次优化咱们筛选了商家根底材料和C后盾2个零碎进行了缓存数据的优化试点,从后果看获得了十分显著的成绩,节俭了大量的硬件资源老本,上面的数据是优化前后的缓存应用状况比照:

商家根底材料Redis数据量由45G降为8G;
C后盾Redis数据量由132G降为7G;

从后果看这个优化的力度太大了,置信大家对如何实现的更加好奇了,那接下来就让咱们一步步来看是如何做到的吧!

首先目前的商家根底材料应用@Caceh注解组件作为缓存形式,它会将从db中查出的值放入本地缓存及jimdb中,因为该组件晚期的版本没有jimdb的默认过期工夫且应用注解时也未显式申明,造成晚期大量的key没有过期工夫,从而造成了大量的僵尸key。
所以如果咱们能够找到这些僵尸key并进行优化,那么就能够将缓存进行一个整体的瘦身,那首先要怎么找出这些key呢?

2.1 keys命令

可能很多同学会想到简略粗犷的keys命令,遍历出所有的key顺次判断是否有过期工夫,但Redis是单线程执行,keys命令会以阻塞的形式执行,遍历形式实现的复杂度是O(n),库中的key越多,阻塞的工夫会越长,通常咱们的数据量都会在几十G以上,显然这种形式是无奈承受的。

2.2 scan命令

redis在2.8版本提供了scan命令,相较于keys命令的劣势:

  • scan命令的工夫复杂度尽管也是O(N),但它是分次进行的,不会阻塞线程。
  • scan命令提供了相似sql中limit参数,能够管制每次返回后果的最大条数。

当然也有毛病:

  • 返回的数据有可能会反复,至于起因能够看文章最初的扩大局部。
  • scan命令只保障在命令开始执行前所有存在的key都会被遍历,在执行期间新增或删除的数据,是不确定的即可能返回,也可能不返回。

2.3根本语法

目前看来这是个不错的抉择,让咱们来看下命令的根本语法:

SCAN cursor [MATCH pattern] [COUNT count]

  • cursor:游标
  • pattern:匹配的模式
  • count:指定从数据集里返回多少元素,默认值为10

2.4 实际

首先感觉上就是依据游标进行增量式迭代,让咱们实际操作下:

看来咱们只须要设置好匹配的key的前缀,循环遍历删除key即可。
能够通过Controller或者调用jsf接口来触发,应用云redis-API,demo如下:

好的,功败垂成.在治理端执行randomkey命令查看.发现仍然存在大量的无用key,貌似还有不少漏网之鱼,这里又是怎么回事呢?

上面又到了脍炙人口的踩坑环节。

2.5 避坑指南

通过减少日发现,返回的后果集为空,但游标并未完结!

其实不难发现scan命令跟咱们在数据库中按条件分页查问是有别的,mysql是依据条件查问出数据,scan命令是按字典槽数顺次遍历,从后果中再匹配出符合条件的数据返回给客户端,那么很有可能在屡次的迭代扫描时没有符合条件的数据。

咱们批改代码应用scanResult.isFinished()办法判断是否曾经迭代实现。

至此程序运行失常,之后通过传入不同的匹配字符,达到分明缓存的目标。

三、课后扩大

这里咱们探讨反复数据的问题:为什么遍历出的数据可能会反复?

3.1 反复的数据

首先咱们看下scan命令的遍历程序:

Redis中有3个key,咱们用scan命令查看发现遍历顺为0->2->1->3,是不是感到奇怪,为什么不是按0->1->2->3的程序?

咱们都晓得HashMap中因为存在hash抵触,当负载因子超过某个阈值时,出于对链表性能的思考会进行Resize操作.Redis也一样,底层的字典表会有动静变换,这种扫描程序也是为了应答这些简单的场景。

3.1.1 字典表的几种状态及应用程序扫描会呈现的问题

  • 字典表没有扩容
    字段tablesize放弃不变,程序扫描没有问题
  • 字典表已扩容实现

假如字典tablesize从8变为16,之前曾经拜访过3号桶,当初0~3号桶的数据曾经rehash到8~11号桶,若果按程序持续拜访4~15号桶,那么这些元素就反复遍历了。

  • 字典表已缩容实现

假如字典tablesize从16放大到8,同样曾经拜访过3号桶,这时8~11号桶的元素被rehash到0号桶,若按程序拜访,则遍历会进行在7号桶,则这些数据就遗漏掉了。

  • 字典表正在Rehashing
    Rehashing的状态则会呈现以上两种问题即要么反复扫描,要么脱漏数据。

3.1.2 反向二进制迭代器算法思维
咱们将Redis扫描的游标与程序扫描的游标转换成二进制作比照:

高位程序拜访是依照字典sizemask(掩码),在无效位上高位加1。

举个例子,咱们看下Scan的扫描形式:
1.字典tablesize为8,游标从0开始扫描;
2.返回客户端的游标为6后,字典tablesize扩容到之前的2倍,并且实现Rehash;
3.客户端发送命令scan 6;

这时scan命令会将6号桶中链表全副取出返回客户端,并且将以后游标的二进制高位加一计算出下次迭代的起始游标.通过上图咱们能够发现扩容后8,12,10号槽位的数据是从之前0,4,2号槽位迁徙过来的,这些槽位的数据曾经遍历过,所以这种遍历程序就防止了反复扫描。

字典扩容的状况相似,但反复数据的呈现正是在这种状况下:

还以上图为例,再看下缩容时Scan的扫描形式:
1.字典tablesize的初始大小为16,游标从0开始扫描;
2.返回客户端的游标为14后,字典tablesize缩容到之前的1/2,并实现Rehash;
3.客户端发送命令scan 14;

这时字典表已实现缩容,之前6和14号桶的数据曾经Rehash到新表的6号桶中,那14号桶都没有了,要怎么解决呢?咱们持续在源码中找答案:

即在找指标桶时总是用以后hashtaba的sizemask(掩码)来计算,v=14即二进制000 1110,以后字典表的掩码从15变成了7即二进制0000 0111,v&m0的值为6,也就是说在新表上还要扫一遍6号桶.然而缩容后旧表6和14号桶的数据都已迁徙到了新表的6号桶中,所以这时扫描的后果就呈现了反复数据,反复的局部为上次未缩容前已扫描过的6号桶的数据。

**论断:
当字典缩容时,高位桶中的数据会合并进低位桶中(6,14)->6,scan命令要保障不脱漏数据,所以要失去缩容前14号桶中的数据,要从新扫描6号桶,所以呈现了反复数据.Redis也挺难的,毕竟鱼和熊掌不可兼得。**

总结

通过本次Redis瘦身实际,尽管是个很小的工具,但的确带来的显著的成果,节约资源降低成本,并且在排查问题中又学习到了命令底层奇妙的设计思维,收货颇丰,最初欢送感兴趣的小伙伴一起交换提高。