当前位置: 首页 > 科技观察

想在生产搞事情?那试试这些 Redis 命令

时间:2023-03-17 19:56:52 科技观察

想在生产中做点什么?然后尝试这些Redis命令。转载本文请联系Java极客技术公众号。哎,最近阿芬又犯事了。事情是这样的,前段时间,阿粉公司的生产交易偶尔会报错。经过一番排查,最终原因是redis命令执行超时。但令人费解的是,生产事务只使用Redis设置的简单命令,这是合理的,不可能执行得这么慢。那么是什么导致了这个问题呢?为了找出这个问题,我们查看分析了最近Redis的慢日志,最终发现耗费了大量时间。命令是keysXX*看到这个命令操作的键的前缀,阿芬发现这是我负责的应用。但是阿芬查了一下,虽然自己的代码没有主动使用keys命令,但是间接使用了底层框架,所以才有了今天这个问题。出现问题的原因是阿芬负责的应用是管理后台应用,使用Shiro框架进行权限管理。由于有多个节点,需要使用分布式会话,所以这里使用Redis来存储会话信息。由于Shiro并没有直接提供Redis存储Session组件,所以阿粉不得不使用Github的一个开源组件shiro-redis。由于Shiro框架需要定期验证Session是否有效,Shiro底层会调用SessionDAO#getActiveSessions获取所有Session信息。而shiro-redis只是继承了SessionDAO接口,底层使用keys命令查找所有存储在Redis中的sessionkey。publicSetkeys(byte[]pattern){checkAndInit();Setkeys=null;Jedisjedis=jedisPool.getResource();try{keys=jedis.keys(pattern);}最后{jedis.close();}returnkeys;}找到问题原因,解决方法比较简单,在github上找到解决方法,将shiro-redis升级到最新版本。在这个版本中,shiro-redis使用scan命令而不是keys来修复这个问题。publicSetkeys(byte[]pattern){Setkeys=null;Jedisjedis=jedisPool.getResource();try{keys=newHashSet();ScanParamsparams=newScanParams();params.count(计数);params.match(模式);byte[]cursor=ScanParams.SCAN_POINTER_START_BINARY;ScanResultscanResult;do{scanResult=jedis.scan(cursor,params);keys.addAll(scanResult.getResult());cursor=scanResult.getCursorAsBytes();}while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START)>0);}finally{jedis.close();}returnkeys;}虽然问题是成功解决了不过阿芬还是有些疑惑。为什么keys命令会减慢其他命令的执行速度?为什么Keys命令查询这么慢?为什么扫描命令没有问题?Redis执行命令的原理首先我们来看第一个问题,为什么keys命令会拖慢其他命令的执行?要回答这个问题,我们先来看看Redis客户端是如何执行一条命令的:从客户端的角度来看,执行命令分为三个步骤:发送命令执行命令并返回结果。同一时刻,可能有多个客户端向Redis发送命令,我们都知道Redis采用的是单线程模型。为了同时处理所有客户端的请求命令,Redis内部采用了队列的方式来排队执行。因此,客户端执行一条命令实际上需要四个步骤:发送命令、排队命令、执行命令、返回结果。由于Redis在单线程中执行命令,它只能从队列开始顺序执行任务。只要3的命令执行速度太慢,队列中的其他任务就得等待。从外部客户端来看,Redis好像被阻塞了,得不到响应。因此,不要使用Redis进程执行长时间运行的指令,这可能会导致Redis阻塞,影响其他指令的执行。KEYS的原理接下来开始回答第二个问题,为什么Keys指令查询这么慢?在回答这个问题之前,请先回忆一下Redis的底层存储结构。在此,阿粉复制了上一篇文章的内容。Redis底层采用字典结构,类似于Java的HashMap底层。keys命令需要返回所有匹配给定模式pattern的Redis中间键。为此,Redis不得不遍历字典中ht[0]哈希表的底层数组。这个的时间复杂度是“O(N)”(N是Redis中key的个数)。Redis中的key数量少的话,执行速度还是会很快的。当Rediskey的数量逐渐增加,上升到数百万、数千万甚至上亿时,执行速度会很慢。下面是阿粉在本地做的一个实验。使用lua脚本将10W个key添加到Redis中,然后使用keys查询所有key。此查询将阻塞大约十秒钟。eval"fori=1,100000doredis.call('set',i,i+1)end"0这里阿粉使用Docker部署Redis,性能可能会稍微差一些。SCAN原理最后,我们来看第三个问题。为什么扫描命令没有问题?这是因为扫描命令使用了一项黑科技——“基于游标的迭代器”。每次调用scan命令时,Redis都会返回一个新的游标和一定数量的键给用户。下次想继续获取剩下的key,需要将这个cursor传递给scan命令,继续之前的迭代过程。简单的说,scan命令使用分页查询redis。下面是scan命令迭代过程的一个例子:scan命令使用一个游标将一个完整的查询巧妙地拆分成多次,以降低查询的复杂度。虽然scan命令的时间复杂度和key一样,都是“O(N)”,但是由于scan命令只需要返回少量的key,所以执行速度会非常快。最后,scan命令虽然解决了key的不足,但也引入了一些其他的缺陷:同一个元素可能会返回多次,这就需要我们的应用增加处理重复元素的能力。如果在迭代过程中向redis添加了一个元素,或者在迭代过程中删除了元素,则返回该元素,也可能不返回。以上缺陷是我们开发中需要考虑的。除了scan,redis还有其他几个增量迭代的命令:sscan:用于迭代当前数据库中的数据库key,用于解决smembers可能出现的阻塞问题hscan命令用于迭代hash中的key-value对key,用于解决hgetall可能出现的阻塞问题。zscan:该命令用于迭代有序集合中的元素(包括元素成员和元素分数),用于生成可能导致阻塞问题的zrange。综上所述,Redis使用单线程来执行操作命令。客户端发送的所有命令都会被Redis放入队列,然后依次从队列中取出对应的命令执行。如果任何任务执行得太慢,就会影响队列中的其他任务。这样,从外部客户端的角度来看,如果Redis的响应延迟,就会显得很阻塞。因此,在生产中不要执行keys、smembers、hgetall、zrange等可能造成阻塞的命令。如果确实需要执行它们,可以使用相应的扫描命令增量遍历,可以有效防止阻塞问题。