Redis学习笔记

158 阅读25分钟

Redis 学习笔记

基本数据类型:string,list ,hash,set,zset

1.string :

Value如果是数据,可以自增(incr) 但是自增有范围,long的界限

2.list:

Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外。

当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为 快速链表 quicklist 的一个结构。

3.hash:

Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞 时,就会将碰撞的元素使用链表串接起来。

不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容 一点点迁移到新的 hash 结构中。

当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

4.set:

Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的 内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。

当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来 存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

5.zset:

zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结 构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权 重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。

zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。 zset 可以用来存 粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间 进行排序。

zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们 可以对成绩按分数进行排序就可以得到他的名次。

zset 内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比 较复杂。

容器型数据结构的通用规则

list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则: 1、create if not exists 如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,

Redis 就会自动创建一个,然后再 rpush 进去新元素。2、drop if no elements 如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一

个元素,列表就消失了。

过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。 需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期, 而不是其中的某个子 key。

还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了

set 方法修改了它,它的过期时间会消失。

分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占 时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用 完了,再调用 del 指令释放茅坑。

使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。从此以后所有的第三方分布式锁 library 可以休息了。 > set lock:codehole true ex 5 nx OK ... do something critical ... > del lock:codehole 上面这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的 奥义所在。

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至 于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁, 但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻 辑执行完之间拿到了锁。

为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数 据出现的小波错乱可能需要人工介入解决。

可重入性

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加 锁,那么这个锁就是可重入的。比如 Java 语言里有个 ReentrantLock 就是可重入锁。Redis 分 布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量 存储当前持有锁的计数。

public class RedisWithReentrantLock { private ThreadLocal lockers = new ThreadLocal<>(); private Jedis jedis; public RedisWithReentrantLock(Jedis jedis) {

this.jedis = jedis; }

private boolean _lock(String key) { return jedis.set(key, "", "nx", "ex", 5L) != null;

}

private void _unlock(String key) { jedis.del(key);

}

private Map <String, Integer> currentLockers() { Map <String, Integer> refs = lockers.get(); if (refs != null) {

return refs; }

lockers.set(new HashMap<>());

return lockers.get(); }

public boolean lock(String key) { Map refs = currentLockers(); Integer refCnt = refs.get(key); if (refCnt != null) {

refs.put(key, refCnt + 1);

return true; }

boolean ok = this._lock(key); if (!ok) {

return false; }

refs.put(key, 1);

return true; }

public boolean unlock(String key) { Map refs = currentLockers(); Integer refCnt = refs.get(key); if (refCnt == null) {

return false; }

refCnt -= 1; if (refCnt > 0) {

refs.put(key, refCnt); } else {

refs.remove(key);

this ._unlock(key); }

return true; }

public static void main(String[] args) { Jedis jedis = new Jedis(); RedisWithReentrantLock redis = new RedisWithReentrantLock(jedis); System.out.println(redis.lock("codehole")); System.out.println(redis.lock("codehole")); System.out.println(redis.unlock("codehole")); System.out.println(redis.unlock("codehole"));

} }

延时队列

Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性, 没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

异步消息队列

Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列, 使用 lpop 和 rpop 来出队列。

可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop, 又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU,redis 的 QPS 也 会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。

有没有什么办法能显著降低延迟呢?你当然可以很快想到:那就把睡觉的时间缩短点。这 种方式当然可以,不过有没有更好的解决方案呢?当然也有,那就是 blpop/brpop。

这两个指令的前缀字符 b 代表的是 blocking,也就是阻塞读。

阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消 息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题。

空闲连接自动断开

如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般

会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常来。 所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。...

上节课我们讲了分布式锁的问题,但是没有提到客户端在处理请求时加锁没加成功怎么办。 一般有 3 种策略来处理加锁失败:

1、直接抛出异常,通知用户稍后重试; 2、sleep 一会再重试; 3、将请求转移至延时队列,过一会再试;

延时队列 这种方式比较适合异步消息处理,将当前冲突的请求扔到另一个队列延后处理以避开冲突。

延时队列的实现

延时队列可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作 为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取到期 的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处 理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行。

高级数据结构:

位图

在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录, 签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。

为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位, 365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大 节约了存储空间。

位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们 可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。

Redis 的位数组是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自 动将位数组进行零扩充

位图就是一个字符串转换成ASCII 码 然后这只1的位置

使用方式:

127.0.0.1:6379> setbit s 1 1 (integer) 0 127.0.0.1:6379> setbit s 2 1 (integer) 0

127.0.0.1:6379> setbit s 4 1 (integer) 0 127.0.0.1:6379> setbit s 9 1 (integer) 0

127.0.0.1:6379> setbit s 10 1 (integer) 0 127.0.0.1:6379> setbit s 13 1 (integer) 0

127.0.0.1:6379> setbit s 15 1 (integer) 0 127.0.0.1:6379> get s "he"

127.0.0.1:6379> setbit w 1 1

127.0.0.1:6379> getbit w 1

127.0.0.1:6379> set w h

127.0.0.1:6379> getbit w 1

统计和查找

Redis 提供了位图统计指令 bitcount 和位图查找指令 bitpos,bitcount 用来统计指定位 置范围内 1 的个数,bitpos 用来查找指定范围内出现的第一个 0 或 1。

127.0.0.1:6379> set w hello OK 127.0.0.1:6379> bitcount w (integer) 21 127.0.0.1:6379> bitcount w 0 0 (integer) 3 # 第一个字符中 1 的位数

127.0.0.1:6379> bitcount w 0 1 (integer) 7 # 第一个字符中 1 的位数 # 前两个字符中 1 的位数

127.0.0.1:6379> bitpos w 0 # 第一个 0 位 (integer) 0 127.0.0.1:6379> bitpos w 1 # 第一个 1 位 (integer) 1

127.0.0.1:6379> bitpos w 1 1 1 (integer) 9 127.0.0.1:6379> bitpos w 1 2 2 (integer) 17

HyperLogLog

但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就 要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。

Redis 提供了 HyperLogLog 数据结构就是用来解决 这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不 精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。

HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用,但是令人感到意外的 是,使用过它的人非常少。

HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加 计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用 户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。

127.0.0.1:6379> pfadd codehole user1 (integer) 1 127.0.0.1:6379> pfcount codehole (integer) 1

127.0.0.1:6379> pfadd codehole user2 (integer) 1 127.0.0.1:6379> pfcount codehole (integer) 2

HyperLogLog 除了上面的 pfadd 和 pfcount 之外,还提供了第三个指令 pfmerge,用于 将多个 pf 计数值累加在一起形成一个新的 pf 值。

比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。 其中页面的 UV 访问量也需要合并,那这个时候 pfmerge 就可以派上用场了。

注意事项

HyperLogLog 这个数据结构不是免费的,不是说使用这个数据结构要花钱,它需要占据 一定 12k 的存储空间,所以它不适合统计单个用户相关的数据。如果你的用户上亿,可以算 算,这个空间成本是非常惊人的。但是相比 set 存储方案,HyperLogLog 所使用的空间那真 是可以使用千斤对比四两来形容了。

布隆过滤器( Bloom Filter)

讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内

容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何

实现推送去重的?

你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户

的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过

的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?

实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查 询,当系统并发量很高时,数据库是很难扛住压力的。

你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间

啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不

缓存的话,性能又跟不上,这该怎么办?

这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。 它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有 一定的误判概率。

布隆过滤器是什么?

布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某 个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合 理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。

当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存 在。打个比方,当它说不认识你时,肯定就不认识;当它说见过你时,可能根本就没见过 面,不过因为你的脸跟它认识的人中某脸比较相似 (某些熟脸的系数组合),所以误判以前见 过你。

Redis 官方提供的布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式登场。布隆过滤 器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。

下面我们来体验一下 Redis 4.0 的布隆过滤器,为了省去繁琐安装过程,我们直接用 Docker 吧。

docker pull redislabs/rebloom # 拉取镜像> docker run -p6379:6379 redislabs/rebloom # 运行容器 > redis-cli # 连接容器中的 redis 服务

如果上面三条指令执行没有问题,下面就可以体验布隆过滤器了。

布隆过滤器基本使用

布隆过滤器有二个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,它的用法 和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要 一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需 要用到 bf.mexists 指令。

127.0.0.1:6379> bf.add codehole user1 (integer) 1 127.0.0.1:6379> bf.add codehole user2 (integer) 1

127.0.0.1:6379> bf.add codehole user3 (integer) 1 127.0.0.1:6379> bf.exists codehole user1 (integer) 1

127.0.0.1:6379> bf.madd codehole user4 user5 user6 1) (integer) 1 2) (integer) 1 3) (integer) 1 127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 1) (integer) 1 2) (integer) 1 3) (integer) 1 4) (integer) 0

我们上面使用的布隆过滤器只是默认参数的布隆过滤器,它在我们第一次 add 的时候自 动创建。Redis 其实还提供了自定义参数的布隆过滤器,需要我们在 add 之前使用 bf.reserve 指令显式创建。如果对应的 key 已经存在,bf.reserve 会报错。bf.reserve 有三个参数,分别

是 key, error_rate 和 initial_size。错误率越低,需要的空间越大。initial_size 参数表示预计放 入的元素数量,当实际数量超出这个数值时,误判率会上升。

所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默 认的 error_rate 是 0.01,默认的 initial_size 是 100。

布隆过滤器的 initial_size 估计的过大,会浪费存储空间,估计的过小,就会影响准确 率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避 免实际元素可能会意外高出估计值很多。

布隆过滤器的 error_rate 越小,需要的存储空间就越大,对于不需要过于精确的场合, error_rate 设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文 章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。

布隆过滤器的原理是通过多个hash 算法通过key算到不通的位置,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这 些位被置为 1 可能是因为其它的 key 存在所致。

布隆计算器地址krisives.github.io/bloom-calcu…

邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平

时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低

简单限流

滑动窗口限流

通过zset (key是用户的行为,value是时间戳),通过指定时间戳来规划滑动窗口,如果窗口内key 为空,zset 会被内存回收掉

漏斗限流

Redis-Cell

该模块只有 1 条指令 cl.throttle,它的参数和返回值都略显复杂,接下来让我们来看看这 个指令具体该如何使用。

上面这个指令的意思是允许「用户老钱回复行为」的频率为每 60s 最多 30 次(漏水速 率),漏斗的初始容量为 15,也就是说一开始可以连续回复 15 个帖子,然后才开始受漏水 速率的影响。我们看到这个指令中漏水速率变成了 2 个参数,替代了之前的单个浮点数。用 两个参数相除的结果来表达漏水速率相对单个浮点数要更加直观一些。

在执行限流指令时,如果被拒绝了,就需要丢弃或重试。cl.throttle 指令考虑的非常周 到,连重试时间都帮你算好了,直接取返回结果数组的第四个值进行 sleep 即可,如果不想 阻塞线程,也可以异步定时任务来重试。

GeoHash(附近的人,事物)

Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现

摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了

在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个 zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一 些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。

Geo 提供了6个指令

增加

geoadd

指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组

127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin (integer) 1 127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader (integer) 1

127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan (integer) 1 127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi (integer) 2

距离

geodist 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。

127.0.0.1:6379> geodist company juejin ireader km "10.5501" 127.0.0.1:6379> geodist company juejin meituan km "1.3878"

127.0.0.1:6379> geodist company juejin jd km "24.2739" 127.0.0.1:6379> geodist company juejin xiaomi km "12.9606"

127.0.0.1:6379> geodist company juejin juejin km "0.0000"

获取元素位置

geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。

127.0.0.1:6379> geopos company juejin

    1. "116.48104995489120483" 2) "39.99679348858259686"

127.0.0.1:6379> geopos company ireader

    1. "116.5142020583152771"
  1. "39.90540918662494363"

127.0.0.1:6379> geopos company juejin ireader

    1. "116.48104995489120483"
  1. "39.99679348858259686"

    1. "116.5142020583152771"
  2. "39.90540918662494363"

获取元素位置

geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。

127.0.0.1:6379> geopos company juejin

    1. "116.48104995489120483" 2) "39.99679348858259686"

127.0.0.1:6379> geopos company ireader

    1. "116.5142020583152771"
  1. "39.90540918662494363"

127.0.0.1:6379> geopos company juejin ireader

    1. "116.48104995489120483"
  1. "39.99679348858259686"

    1. "116.5142020583152771"
  2. "39.90540918662494363"

获取元素的 hash 值

geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32 编码。 你可 以使用这个编码值去 geohash.org/${hash}中进行直… geohash 的标准编码 值。

127.0.0.1:6379> geohash company ireader

  1. "wx4g52e1ce0" 127.0.0.1:6379> geohash company juejin

  2. "wx4gd94yjn0"

附近的公司

georadiusbymember 指令是最为关键的指令,它可以用来查询指定元素附近的其它元 素,它的参数非常复杂。

范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身

127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc 1) "ireader" 2) "juejin" 3) "meituan"

范围 20 公里以内最多 3 个元素按距离倒排

127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc 1) "jd" 2) "meituan" 3) "juejin"

Redis 还提供了根据坐标值来 查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近 的餐馆」等。它的参数和 georadiusbymember 基本一致,除了将目标元素改成经纬度坐标 值。

127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc

    1. "ireader"
  1. "0.0000"

    1. "juejin"
  2. "10.5501"

    1. "meituan"
  3. "11.5748"

注意事项

在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合 可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成 较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现 卡顿现象,影响线上服务的正常运行。

所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按 市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。

大海捞针 —— Scan

入门

在平时线上 Redis 维护工作中,有时候需要从 Redis 实例成千上万的 key 中找出特定 前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问 题,如何从海量的 key 中找出满足特定前缀的 key 列表来?

Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。

127.0.0.1:6379> keys codehole*

  1. "codehole1"

  2. "codehole3" 3) "codehole2" 127.0.0.1:6379> keys code*hole

  3. "code3hole" 2) "code2hole" 3) "code1hole"

这个指令使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点。 1、没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。

keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令

就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为

Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才

可以继续。

它在 2.8 版本中加入了大海捞针的指令 ,scan 相比 keys 具备有以下特点:

1、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;

2、提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的 结果可多可少; (它相当于把内存分成很多块,过滤出没一块重符合条件的,所以每一块的数量不固定)

3、同 keys 一样,它也提供模式匹配功能;

4、服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;

5、返回的结果可能会有重复,需要客户端去重复,这点非常重要;

6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;

7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

使用

an 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三

个是遍历的 limit hint。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为

下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

127.0.0.1:6379> scan 0 *match key99 count 1000

  1. "13976"

  2. "key9911"

  3. "key9974"

  4. "key9994"

  5. "key9910"

  6. "key9907"

6)"key9989"

  1. "key9971"

  2. "key99"

  3. "key9966"

  4. "key992"

  5. "key9903"

  6. "key9905"

127.0.0.1:6379> scan 13976 match key99* count 1000

  1. "1996"

  2. "key9982"

  3. "key9997"

  4. "key9963"

  5. "key996"

  6. "key9912"

  7. "key9999"

  8. "key9921"

  9. "key994"

  10. "key9956"

  11. "key9919"

扩容之渐进式 rehash

Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如 果 HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐 进式 rehash。

它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐 地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于 rehash 中的字典,需要同 时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。

scan 也需要考虑这个问题,对与 rehash 中的字典,它需要同时扫描新旧槽位,然后将 结果融合后返回给客户端。

更多的 scan 指令

scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进 行遍历。比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集 合的元素。

它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的 hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容,所以 这里不再赘述

大 key 问题

大key在redis 扩容,集群迁移,系统回收的时候回损耗性能(redis 实际上还是单线程的),redis 只有多进程的概念,造主从复制、AOP、ROF之类的场景会使用

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys

如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参 数。

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1

上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的 时间会变长。

线程 IO 模型

非阻塞 IO

当我们调用套接字的读写方法,默认它们是阻塞的,比如 read 方法要传递进去一个参数 n,表示读取这么多字节后再返回,如果没有读够线程就会卡在那里,直到新的数据到来或者 连接关闭了,read 方法才可以返回,线程才能继续处理。而 write 方法一般来说不会阻塞,除 非内核为套接字分配的写缓冲区已经满了,write 方法就会阻塞,直到缓存区中有空闲空间挪 出来了。

非阻塞 IO 在套接字对象上提供了一个选项 Non_Blocking,当这个选项打开时,读写方 法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的 读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节 数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。

有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线 程可以继续干别的事了。

事件轮询 (多路复用)

非阻塞 IO 有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道 何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满 了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。

事件轮询 API 就是用来解决这个问题的,最简单的事件轮询 API 是 select 函数,它是 操作系统提供给用户程序的 API。输入是读写描述符列表 read_fds & write_fds,输出是与之 对应的可读可写事件。同时还提供了一个 timeout 参数,如果没有任何事件到来,那么就最多 等待 timeout 时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过 了之后还是没有任何事件到来,也会立即返回。拿到事件后,线程就可以继续挨个处理相应 的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事 件循环,一个循环为一个周期。

因为我们通过 select 系统调用同时处理多个通道描述符的读写事件,因此我们将这类系 统调用称为多路复用 API。现代操作系统的多路复用 API 已经不再使用 select 系统调用,而 改用 epoll(linux)和 kqueue(freebsd & macosx),因为 select 系统调用的性能在描述符特别多时

性能会非常差。它们使用起来可能在形式上略有差异,但是本质上都是差不多的,都可以使

用上面的伪代码逻辑进行理解。

服务器套接字 serversocket 对象的读操作是指调用 accept 接受客户端新连接。何时有新连 接到来,也是通过 select 系统调用的读事件来得到通知的。

事件轮询 API 就是 Java 语言里面的 NIO 技术

Java 的 NIO 并不是 Java 特有的技术,其它计算机语言都有这个技术,只不过换了一 个词汇,不叫 NIO 而已。

指令队列

Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行 顺序处理,先到先服务 。

响应队列

Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将 指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要 去获取写事件,也就是可以将当前的客户端描述符从 write_fds 里面移出来。等到队列有数据 了,再将描述符放进去。避免 select 系统调用立即返回写事件,结果发现没什么数据可以 写。出这种情况的线程会飙高 CPU。

持久化

Redis 的持久化机制有两种,第一种是快照(RDB),第二种是 AOF 日志。快照是一次全量备 份,AOF 日志是连续的增量备份。

快照是内存数据的二进制序列化形式,在存储上非常紧 凑,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会 变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。 所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。

快照原理

Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制 很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。

fork(多进程)

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进 程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代 码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。这是 Linux 操作系 统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的

增长几乎没有明显变化。

AOF 原理

AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的 指令记录。

Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文 本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已 经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。

Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦 身。

AOF 重写

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进 程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。 序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加 完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

fsync

这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个 时候就会出现日志丢失。那该怎么办?

Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁 盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个 磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的 地位就不保了。

所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能 使得数据少丢失。

Redis 4.0 混合持久化

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常 使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实 例很大的情况下,启动需要花费很长的时间。

Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文 件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自 持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

管道 (Pipeline)

管道提升性能的原理是调整读写顺序,合并多次堵,多次写,变成一次网络通讯,不然每次读写都是一次网络通讯,每次都需要等待read 的IO超时,所以管道中的读写越多

性能越好。当然并不是无上限,应为有缓存区存在,write 缓存区满了,也得IO 等待。

这便是管道操作的本质,服务器根本没有任何区别对待,还是收到一条消息,执行一条 消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大 幅节省 IO 时间。管道中指令越多,效果越好。

深入理解管道本质

我们开始以为 write 操作是要等到对方收到消息才会返回,但实际上不是这样的。write 操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统 内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间 来,这个就是写操作 IO 操作的真正耗时。

我们开始以为 read 操作是从目标机器拉取数据,但实际上不是这样的。read 操作只负 责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就 需要等待数据到来,这个就是读操作 IO 操作的真正耗时。

所以对于 value = redis.get(key)这样一个简单的请求来说,write 操作几乎没有耗时,直接 写到发送缓冲就返回,而 read 就会比较耗时了,因为它要等待消息经过网络路由到目标机器 处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开 销。

而对于管道来说,连续的 write 操作根本就没有耗时,之后第一个 read 操作会等待一个 网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作 直接就可以从缓冲拿到结果,瞬间就返回了。

事务

所有的指令在 exec 之前不执行,而是缓存在 服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕 后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列 的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。

Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔 离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利

Watch(乐观锁)

redis 分布式锁是悲观锁,而Watch是乐观锁。watch 会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到 了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被 修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null 回复告知客户端事务执行失败,这个时候客户端一般会选择重试。

Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键 变量,否则会出错。

消息队列

近期 Redis5.0 新增了 Stream 数据结构,这个功能给 Redis 带来了持久化消息队列

小对象压缩

Redis 如果使用 32bit 进行编译,内部所有数据结构所使用的指针空间占用会少一半, 如果你对 Redis 使用内存不超过 4G,可以考虑使用 32bit 进行编译,可以节约大量内存。 4G 的容量作为一些小型站点的缓存数据库是绰绰有余了,如果不足还可以通过增加实例的 方式来解决。

内存回收机制

Redis的内存是划分成很多个页面,每个页面上有很多个Key,只有当一个页面上的所有Key 都没了才会回收这个页面,所以可能存在总共内存10个G,当删除1个Key后发现内存几乎没有变化这种极端情况。

集群

哨兵(Sentinel )

我们可以将 Redis Sentinel 集群看成是一个 ZooKeeper 集群,它是集群高可用的心脏, 它一般是由 3~5 个节点组成,这样挂了个别节点集群还可以正常运转。

它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为 主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址, 然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地 址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节 点切换。

消息丢失

Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消 息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别 多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以 限制主从延迟过大。

min-slaves-to-write 1 min-slaves-max-lag 10

第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服

务,丧失可用性。

何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如 果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没 有给反馈。

Cluster

相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成, 每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相 互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。

Redis Cluster 将所有数据划分为 16384 的 slots ,当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当 客户端要查找某个 key 时,可以直接定位到目标节点。

容错

Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其 中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完 全处于不可用状态。不过 Redis 也提供了一个参数 cluster-require-full-coverage 可以允许部分 节点故障,其它节点还可以继续提供对外访问。

(广播消息队列)Stream

它是一个新的强大的支持多播的可持久化的消息队列,作者坦言 Redis Stream 狠狠地借鉴了 Kafka 的设计。

Redis Stream 的 消息是持久化的,Redis 重启后,内容还在。

每个 Stream 都可以挂多个消费组,每个消费组会有个游标 last_delivered_id 在 Stream 数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个 Stream 内唯一的名称,消费组不会自动创建,它需要单独的指令 xgroup create 进行创建,需要指定 从 Stream 的某个消息 ID 开始消费,这个 ID 用来初始化 last_delivered_id 变量。

每个消费组 (Consumer Group) 的状态都是独立的,相互不受影响。也就是说同一份 Stream 内部的消息会被每个消费组都消费到。

同一个消费组 (Consumer Group) 可以挂接多个消费者 (Consumer),这些消费者之间是 竞争关系,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。每个消费者有 一个组内唯一名称

消费者 (Consumer) 内部会有个状态变量 pending_ids,它记录了当前已经被客户端读取 的消息,但是还没有 ack。如果客户端没有 ack,这个变量里面的消息 ID 会越来越多,一 旦某个消息被 ack,它就开始减少。这个 pending_ids 变量在 Redis 官方被称之为 PEL,也 就是 Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一 次,而不会在网络传输的中途丢失了没处理。

增删改查

1、xadd 追加消息

2、xdel 删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度

3、xrange 获取消息列表,会自动过滤已经删除的消息

4、xlen 消息长度

5、del 删除 Stream

独立消费

我们可以在不定义消费组的情况下进行 Stream 消息的独立消费,当 Stream 没有新消 息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令 xread,可以将 Stream 当成普 通的消息队列 (list) 来使用。使用 xread 时,我们可以完全忽略消费组 (Consumer Group) 的存在,就好比 Stream 就是一个普通的列表 (list)。

创建消费组

Stream 通过 xgroup create 指令创建消费组 (Consumer Group),需要传递起始消息 ID 参数用 来初始化 last_delivered_id 变量。

表示从头开始消费

127.0.0.1:6379> xgroup create codehole cg1 0-0 OK # 表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略127.0.0.1:6379>xgroupcreatecodeholecg2表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略 127.0.0.1:6379> xgroup create codehole cg2 OK

消费

Stream 提供了 xreadgroup 指令可以进行消费组的组内消费,需要提供消费组名称、消 费者名称和起始消息 ID。它同 xread 一样,也可以阻塞等待新消息。读到新消息后,对应 的消息 ID 就会进入消费者的 PEL(正在处理的消息) 结构里,客户端处理完毕后使用 xack 指令通知服务器,本条消息已经处理完毕,该消息 ID 就会从 PEL 中移除。

要是消息积累太多,Stream 的链表岂不是很长,内容会不会爆掉?xdel 指令又不会删除消息,它只是给消息做了个标志位。

Redis 自然考虑到了这一点,所以它提供了一个定长 Stream 功能。在 xadd 的指令提供 一个定长长度 maxlen,就可以将老的消息干掉,确保最多不超过指定长度。

Info 指令

在使用 Redis 时,时常会遇到很多问题需要诊断,在诊断之前需要了解 Redis 的运行状 态,通过强大的 Info 指令,你可以清晰地知道 Redis 内部一系列运行参数。

1、Server 服务器运行的环境参数 2、Clients 客户端相关信息 3、Memory 服务器运行内存统计数据 4、Persistence 持久化信息

5、Stats 通用统计数据

6、Replication 主从复制相关信息

7、CPU CPU 使用情况

8、Cluster 集群信息

9、KeySpace 键值对统计数量信息Info 可以一次性获取所有的信息,也可以按块取信息。

获取所有信息

info

获取内存相关信息

info memory

获取复制相关信息

再谈分布式锁

不过在集群环境下,这种方式是有缺陷的,它不是绝对安全的。

比如在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感 知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节 点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当 另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户 端同时持有,不安全性由此产生。

Redlock(红锁) 算法

为了使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。 同很多分布式算法一样,redlock 也使用「大多数机制」。

加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还 需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读 写,意味着相比单实例 Redis 性能会下降一些

过期策略

因为 Redis 是单线程的,收割的时间也会占用线程的处理时间,如果收割的太 过于繁忙,会不会导致线上读写指令出现卡顿。

过期的 key 集合

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个 字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓 惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期 了就立即删除。定时删除是集中处理,惰性删除是零散处理。

定时扫描策略

Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

1、从过期字典中随机 20 个 key; 2、删除这 20 个 key 中已经过期的 key;

3、如果过期的 key 比率超过 1/4,那就重复步骤 1;

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时 间的上限,默认不会超过 25ms。

所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置 一个随机范围,而不能全部在同一时间过期。

在目标过期时间上增加一天的随机时间

redis.expire_at(key, random.randint(86400) + expire_ts)

在一些活动系统中,因为活动是一期一会,下一期活动举办时,前面几期的很多数据都 可以丢弃了,所以需要给相关的活动数据设置一个过期时间,以减少不必要的 Redis 内存占 用。如果不加注意,你可能会将过期时间设置为活动结束时间再增加一个常量的冗余时间, 如果参与活动的人数太多,就会导致大量的 key 同时过期。

从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

删除

Redis 提供了 flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。 Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树 连根拔起,扔给后台线程慢慢焚烧。

flushall async OK

Jedis

Java 程序一般都是多线程的应用程序,意味着我们很少直接使用 Jedis,而是要用到 Jedis 的连接池 —— JedisPool。同时因为 Jedis 对象不是线程安全的,当我们要使用 Jedis 对象时,需要从连接池中拿出一个 Jedis 对象独占,使用完毕后再将这个对象还给连接池。

重试

我们知道 Jedis 默认没有提供重试机制,意味着如果网络出现了抖动,就会大范围报 错,或者一个后台应用因为链接过于空闲被服务端强制关闭了链接,当重新发起新请求时就 第一个指令会出错。而 Redis 的 Python 客户端 redis-py 提供了这种重试机制,redis-py 在 遇到链接错误时会尝试进行重连,然后再重发指令。

那如果我们希望在 Jedis 上面增加重试机制,该如何做呢?有了上面的 RedisPool 对 象,重试就非常容易进行了。

(Redis集群间安全通讯)spiped

Redis间通讯不能暴露在公网,有安全隐患,如果需要多个Redis间通讯,则需要使用spiped