马什么梅?
.-' _..`.
/ .'_.'.'
| .' (.)`.
;' ,_ `.
.--.______.' ; `.;-'
| ./ /
| | /
`..'`-._ ___, ..'
/ | | | |\ \
/ /| | | | \ \
/ / | | | | \ \
/_/ |_| |_| \_\
|_\ |_\ |_\ |_\
1.使用spring stringRedisTemplate 对redis操作出现的连接不释放问题:
spring stringRedisTemplate 对redis常规操作做了一些封装,但还不支持像 Scan SetNx等命令,这时需要拿到jedis Connection进行一些特殊的Commands
使用 stringRedisTemplate.getConnectionFactory().getConnection() 是不被推荐的
我们可以使用
| ```html stringRedisTemplate.execute(new RedisCallback() { @Override public Cursor doInRedis(RedisConnection connection) throws DataAccessException { return connection.scan(options); } })
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
```html
来执行,
或者使用完connection后 ,用
| ```html RedisConnectionUtils.releaseConnection(conn, factory);
| ------------------------------------------------------------------ |
来释放connection.
同时,redis中也不建议使用keys命令,redis pool的配置应该合理配上,否则出现问题无错误日志,无报错,定位相当困难。
2.过期键的删除策略
定时删除
在设置键过去的时间同时,创建一个定时器,让定时器在键过期时间来临,立即执行对键的删除操作。
创建一个定时器需要用到 Redis 服务器中的时间事件,而当前时间事件的实现方式是无序链表,时间复杂度为 O(n),让服务器大量创建定时器去实现定时删除策略,会产生较大的性能影响,所以,定时删除并不是一种好的删除策略。
惰性删除
在取出该键的时候对键进行过期检查,即只对当前处理的键做删除操作,不会在其他过期键上花费 CPU 时间
**缺点:** 对内存不友好,如果一但键过期了,但会保存在内存中,如果这个键还不会被访问,那么久会造成内存浪费,甚至造成内存泄露
| ```html
int expireIfNeeded(redisDb *db, robj *key) { // 键未过期返回0 if (!keyIsExpired(db,key)) return 0; // 如果运行在从节点上,直接返回1,因为从节点不执行删除操作,可以看下面的复制部分 if (server.masterhost != NULL) return 1; // 运行到这里,表示键带有过期时间且运行在主节点上 // 删除过期键个数 server.stat_expiredkeys++; // 向从节点和AOF文件传播过期信息 propagateExpire(db,key,server.lazyfree_lazy_expire); // 发送事件通知 notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); // 根据配置(默认是同步删除)判断是否采用惰性删除(这里的惰性删除是指采用后台线程处理删除操做,这样会减少卡顿) return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); }
``` |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 我们通常说 Redis 是单线程的,其实 Redis 把处理网络收发和执行命令的操作都放到了主线程,但 Redis 还有其他后台线程在工作,这些后台线程一般从事 IO 较重的工作,比如刷盘等操作。上面源码中根据是否配置 lazyfree_lazy_expire(4.0版本引进) 来判断是否执行惰性删除,原理是先把过期对象进行逻辑删除,然后在后台进行真正的物理删除,这样就可以避免对象体积过大,造成阻塞,后面会在深入研究下 Redis 的 lazyfree 原理 源码位置 lazyfree.c/dbAsyncDelete 方法 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
定期删除
定期策略是每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU 时间的影响,同时也减少了内存浪费
Redis 默认会每秒进行 10 次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典中的所有键,而是采用了如下方法
1. 从过期字典中随机取出 20 个键
2. 删除这 20 个键中过期的键
3. 如果过期键的比例超过 25% ,重复步骤 1 和 2
为了保证扫描不会出现循环过度,导致线程卡死现象,还增加了扫描时间的上限,默认是 25 毫秒(即默认在慢模式下,如果是快模式,扫描上限是 1 毫秒)
| ```html
void activeExpireCycle(int type) { ... do { ... if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) // 选过期键的数量,为 20 num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; while (num--) { dictEntry *de; long long ttl; // 随机选 20 个过期键 if ((de = dictGetRandomKey(db->expires)) == NULL) break; ... // 尝试删除过期键 if (activeExpireCycleTryExpire(db,de,now)) expired++; ... } ... // 只有过期键比例 < 25% 才跳出循环 } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } ... }
``` |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 因为 Redis 在扫描过期键时,一般会循环扫描多次,如果请求进来,且正好服务器正在进行过期键扫描,那么需要等待 25 毫秒,如果客户端设置的超时时间小于 25 毫秒,那就会导致链接因为超时而关闭,就会造成异常,这些现象还不能从慢查询日志(之前分享过慢查询日志的文章 [Redis慢查询日志](http://mp.weixin.qq.com/s?\__biz=MzU1MzUzODM2NQ==\&mid=2247483921\&idx=1\&sn=b345a6dd97fc25db71082c63ff17d544\&chksm=fbf00443cc878d55a943a9f9e18c0e666cdff6a17323b844e5813f4cbeddb6a6c742707588b1\&scene=21#wechat_redirect))中查询到,因为慢查询只记录逻辑处理过程,不包括等待时间。所以我们在设置过期时间时,一定要避免同时大批量键过期的现象,所以如果有这种情况,最好给过期时间加个随机范围,缓解大量键同时过期,造成客户端等待超时的现象 |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Redis 过期键删除策略
Redis 服务器采用惰性删除和定期删除这两种策略配合来实现,这样可以平衡使用 CPU 时间和避免内存浪费
主服务器和从服务器:从服务器不会主动执行删除,遇见惰性删除也不会删,保证数据的一致性
主从服务器链接断开怎么办?
Redis 采用 PSYNC 命令来执行复制时的同步操作,当从服务器在断开后重新连接主服务器时,主服务器会把从服务器断线期间执行的写命令发送给从服务器,然后从服务器接收并执行这些写命令,这样主从服务器就会达到一致性,那主服务器如何判断从服务器断开链接的过程需要哪些命令?主服务器会维护一个固定长度的先进先出的队列,即复制积压缓冲区,缓冲区中保存着主服务器的写命令和命令对应的偏移量,在主服务器给从服务器传播命令时,同时也会往复制积压缓冲区中写命令。从服务器在向主服务器发送 PSYNC 命令时,同时会带上它的最新写命令的偏移量,这样主服务器通过对比偏移量,就可以知道从服务器从哪里断开的了
发生网络抖动,主服务器发送的 del 命令没有传递到从服务器怎么办?
其实主从服务器之间会有心跳检测机制,主从服务器通过发送和接收 REPLCONF ACK 命令来检查两者之间的网络连接是否正常。当从服务器向主服务器发送 REPLCONF ACK 命令时,主服务器会对比自己的偏移量和从服务器发过来的偏移量,如果从服务器的偏移量小于自己的偏移量,主服务器会从复制积压缓冲区中找到从服务器缺少的数据,并将数据发送给从服务器,这样就达到了数据一致性
RDB文件和AOF文件:RDB是载入时就检查,从同步主的命令;AOF是载入时追加del命令,重写过程不会保存过期键
3.Redis最易被忽视的CPU和内存占用高问题
短连接
| ./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 1(k=1表示使用长连接,k=0表示使用短连接) |
| ---------------------------------------------------------------------------------------- |
k=1:占用CPU最高的是readQueryFromClient,即主要是在处理来自用户端的请求。
k=0:占用CPU最高的确实是listSearchKey,而readQueryFromClient所占CPU的比例比listSearchKey要低得多,也就是说CPU有点“不务正业”了,处理用户请求变成了副业,而搜索list却成为了主业。所以在同样的业务请求量下,使用短连接会增加CPU的负担。
缺点:
redis-server端在client close的时候,需要进行一个o(n)的操作,去寻找close的client链接,而server的list\<client>由 --maxclients配置默认10000。(ps:这种操作在当用户的链接池被打满之后,用户端就会尝试着使用短连接)
info命令导致CPU高
| 有用户通过定期执行info命令监视redis的状态,这会在一定程度上导致CPU占用偏高。频繁执行info时通过perf分析发现getClientsMaxBuffers、getClientOutputBufferMemoryUsage及getMemoryOverheadData这几个函数占用CPU比较高 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- |
实验:
当1个空连接info,CPU仅为20%
当9999个空连接,CPU可达到80%
所以在连接数较高时,尽量避免使用info命令
pipeline导致内存占用高
| redis-server端从接收到的内容依次解析出命令、执行命令、将执行结果缓存到replyBuffer中,并将用户端标记为有内容需要写出。等到下次事件调度时再将replyBuffer中的内容通过socket发送到client,所以并不是处理完一条命令就将结果返回用户端。 |
| ---------------------------------------------------------------------------------------------------------------------------------------- |
如果用户端程序处理比较慢,未能及时通过c.Receive()从TCP的接收buffer中读取内容或者因为某些BUG导致没有执行c.Receive(),当接收buffer满了后,server端的TCP滑动窗口为0,导致server端无法发送replyBuffer中的内容,所以replyBuffer由于迟迟得不到释放而占用额外的内存。当pipeline一次打包的命令数太多,以及包含如mget、hgetall、lrange等操作多个对象的命令时,问题会更突出。
4.Redis 为什么用跳表而不用平衡树?
skipList
| 一般查找问题的解法分为两个大类:一个是基于各种平衡树,一个是基于哈希表。但skiplist却比较特殊,它没法归属到这两大类里面。skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。 |
| -------------------------------------------------------------------------------------------------------------- |
skipList为了避免增加删除时调整相邻两层链表上节点个数严格的2:1的对应关系,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level),
除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。
[随机数的过程:](https://juejin.im/post/57fa935b0e3dd90057c50fbc)
* 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
* 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。
* 节点最大的层数不允许超过一个最大值,记为MaxLevel。
优点:
* skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
* 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
* 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
* 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
* 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
* 从算法实现难度上来比较,skiplist比平衡树要简单得多。
5.Redis的事务和ACID的关系
watch
WATCH命令用于在事务开始之前监视任意数量的键:当调用EXEC命令执行事务时,如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行,直接返回失败。 Redis内部通过维护一个watched_keys字典来记录所有被WATCH的key,value对应的一个是单链表,链表的值为客户端的信息。虽然Redis 是串行执行事务内容,并且执行的时候不会被其他操作打断,但是由于执行和声明WATCH是有时间差的,导致整个时间差内会有被其他客户端修改被WATCH的key的可能的。
* 如果客户端的REDIS_DIRTY_CAS选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
* 如果REDIS_DIRTY_CAS选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
multi
Redis的事务是不可嵌套的,当客户端已经处于事务状态,而客户端又再向服务器发送MULTI时,服务器只是简单地向客户端发送一个错误,然后继续等待其他命令的入队。MULTI 命令的发送不会造成整个事务失败,也不会修改事务队列中已有的数据。(watch一样)
原子性:虽然Redis的单个命令是原子的,并且也保证事务内的操作是顺序执行、不会被干扰,但是它并没有在事务上增加原子的特性。也就是说,如果如发生Redis进程被kill 或者宿主机器宕机等情况,已经执行完的操作是不会被回滚或者重试的。
一致性:如果命令在事务执行的过程中发生错误,比如说,对一个不同类型的key执行了错误的操作,那么Redis 只会将错误包含在事务的结果中,这不会引起事务中断或整个失败(服务器不会判断key的类型是否可以做对应的Op操作 ),不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响。
Redis进程被终结:
内存模式:数据都没有了,肯定一致
RDB模式:在执行事务时,Redis不会中断事务去执行保存RDB的工作,只有在事务执行之后,保存RDB的工作才有可能开始。所以没有就是没有,有就是有
AOF模式:两种情况,A执行完了或者没执行(都是一致的);B执行了一半(AOF的文件就不对需要redis-check-aof检查修正)
隔离性:Redis是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis的事务是总是带有隔离性的。
持久性:因为事务不过是用队列包裹起了一组Redis命令,并没有提供任何额外的持久性功能,所以事务的持久性由Redis所使用的持久化模式决定。
6.Redis的乐观锁Watch如何实现?
CAS 操作包含三个操作数:
* 内存位置的值(V)
* 预期原值(A)
* 新值(B)
在 Redis 的事务中使用 Watch 实现,Watch 会在事务开始之前盯住 1 个或多个关键变量。
当事务执行时,也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时, Redis 会检查关键变量自 Watch 之后,是否被修改了。
对比java的native源码的CAS部分:Cmpxchg 是一条 CPU 指令的命令而不是多条 CPU 指令,所以它不会被多线程的调度所打断,所以能够保证 CAS 的操作是一个原子操作。
redis中:Redis 的 Watch 不存在 ABA 问题,也没有多次重试机制,其中有一个重大的不同是:Redis 事务执行其实是串行的。
RedisDb 存放了一个 watched_keys 的 Dcit 结构,每个被 Watch 的 Key 的值是一个链表结构,存放的是一组 Redis 客户端标志。每一次 Watch,Multi,Exec 时都会去查询这个 watched_keys 结构进行判断,每次 Touch 到被 Watch 的 Key 时都会标志为 CLIENT_DIRTY_CAS。
7.一次pipeline携带多少个HGETALL命令才会发起一次I/O?
务器吞吐量大了,可能就会导致qps急剧下降(网卡大量收发数据和redis内部协议解析,redis命令排队堆积,从而导致的缓慢),而想要qps高,服务器吞吐量可能就要降下来,无法很好的利用带宽。
对两者之间的取舍,同样是不能拍脑袋决定的,用压测数据说话!
pipeline批量请求的key可能分布在不同的机器上,但pipeline请求最终可能只被一台redis server处理,那不就是会读取数据失败吗?
答:redis cluster 是官方给出的分布式方案。 Redis Cluster在设计中没有使用一致性哈希,而是使用数据分片(Sharding)引入哈希槽(hash slot)来实现。一个 Redis Cluster包含16384(0\~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16 key/16384来计算key属于哪个槽。比如redis cluster有5个节点,每个节点就负责一部分哈希槽,**如果参数的多个key在不同的slot,在不同的主机上,那么必然会出错。** **因此redis cluster分布式方案是不支持pipeline操作**,如果想要做,只有客户端缓存slot和redis节点的关系,在批量请求时,就通过key算出不同的slot以及redis节点,并行的进行pipeline。