Redis - Quick Review(含LRU、分片、常见场景说明)

AI 摘要: Redis是一款支持多种数据结构的内存存储系统,具有高可用性和高性能。本文概述了Redis的基本特性和常见的数据操作方式。

1. Redis概述

Redis是一款支持多种数据结构的内存存储系统,基于C语言编写,单线程服务,可用于数据缓存、数据存储、消息中间件等功能,支持常见数据类型有:字符串、List、Hashes表、集合、有序集合Zsort等,另外不常用的HyperLogLogs、BitMaps、GEO地理空间位置。

同时Redis服务支持内置主从负载,通过集群、哨兵等多种内置部署模式,提供服务的高可用;

Redis可以使用管道、LUA脚本来加速Redis的执行(CS模式,合并操作指令,降低TTL);

Redis仅支持LRU算法回收内存,在超过内存大小配置,按一定策略驱逐内存key,回收内存;

Redis键不存在,则创建,若聚合类型元素为空(比如LPOP或SREM等),则键被删除;

键命名很关键,过长影响性能,过短可读性较差,选择适中方式;

2. Redis数据类型说明 1

2.1. Keys操作

  • 删:DEL
  • 查:KEYS、EXISTS、TTL、PTTL、TYPE
    • 迭代查:SCAN/HSCAN/SSCAN/ZSCAN(解决KEYS、SMEMBERS阻塞服务器)
  • 改:RENAME、RENAMNX、TOUCH(更新键访问时间)
  • 过期:EXPIRE、EXPIREAT、PEXPIRE、PEXPIREAT
  • 持久化:PERSIST
  • 迁移:MIGRATE(迁移)、MOVE
  • 排序:SORT(返回列表、集合、有序集合 key 中经过排序的元素)
  • 备份恢复:DUMP(导出)、RESTORE
1
2
3
4
5
127.0.0.1:6379> SORT webcheck ALPHA
1) "www.alibaba.com"
2) "www.baidu.com"
3) "www.qq.com"
4) "www.tmall.com"

2.2. String字符串

  • 增:INCR、INCRBY、INCRBYFLOAT
  • 减:DECR、DECRBY
  • (批量)设置: SET、MSET、GETSET
  • (批量)获取:GET、MGET、
  • NX/EX: SETEX(秒级覆盖写入)、PSETEX(毫秒级)、SETNX(不存在设置)、MSETNX(批量设置不存在key)
  • RANGE: SETRANGE、GETRANGE
  • BIT: BITCOUNT、BITOP、SETBIT、GETBIT
  • 其他:APPEND(字符串增加)、STRLEN(字符串长度)

2.3. HASH表

  • 删:HDEL
  • (批量)设置/获取:HSET、HMSET、HGET、HMGET(指定多个项)、HGETALL(所有的项和值)
  • 改:HINCRBY、HINCRBYFLOAT(支持添加负数)
  • 其他:
    • HLEN(项数)、HKEYS(返回项)、HVALS(返回值)、HEXISTS(项检查)、HSCAN(迭代hash)
    • HSETNX(不存在才设置)

2.4. LIST列表

  • 插入列表(首、尾):LPUSH、RPUSH、LPUSHX(仅列表存在才插入)、RPUSHX
  • 移除列表(首、尾):LPOP、RPOP
  • 阻塞移除:BLPOP、BRPOP、BRPOPLPUSH(可做安全队列)
  • 截断:LTRIM
  • 查:LRANGE、LLEN、LINDEX
  • 删:LREM
  • 改:LSET(设置索引元素值)、LINSERT(中间插入)、

2.5. Set集合

  • 增:SADD
  • 查:SCARD(元素数量)、SMEMBERS(所有元素)、SSCAN(游标迭代输出)、SISMEMBER(元素存在确定)、SRANDMEMBER(随机取一个元素,不删除)
  • 删:SREM(删除指定的元素)、SPOP(随机pop指定数量元素)、SMOVE(从集合A移到集合B)
  • 集合操作:交(SINTER)、并(SUNION)、差(SDIFF),以及结果操作结果存储到另一集合(SINTERSTORE/SUNIONSTORE/SDIFFSTORE)

2.6. SortedSet集合

  • 增:ZADD(支持键、分数检测条件添加)
  • 改:ZINCRBY(增加成员得分)
  • 查:ZCARD(元素数量)、ZSCORE(元素得分)
    • 正向查(从低到高):ZRANGE(返回元素列表)、ZRANGEBYSCORE、ZRANGEBYLEX
    • 反向查(从高到低):ZREVRANGE、ZREVRANGEBYSCORE(在指定范围内的得分元素排序返回)
    • SCORE(得分):ZCOUNT、ZRANGEBYSCORE、ZREMRANGEBYSCORE(基于得分查、删)
    • LEX(字典):ZLEXCOUNT、ZRANGEBYLEX、ZREMRANGEBYEX(按字典顺序查、删)
    • RANK(索引):ZRANK(集合索引,从小大到,从0开始)、ZREMRANGEBYRANK(基于索引查、删)
  • 删:ZREM(删除指定元素)、ZPOPMAX(弹出最高得分的N个元素)、ZPOPMIN(弹出最小N个元素)
  • 集合操作:交、并

3. Redis特性支持

3.1. Pub/Sub

  • 订阅:SUBSCRIBE、PSUBSCRIBE(pattern模式订阅)
    • 取消订阅:UNSUBSCRIBE、PUNSUBSCRIBE
  • 发布:PUBLISH
  • 信息检查:PUBSUB
    • PUBSUB CHANNELS [pattern]:当前活动通道情况(不含模式订阅匹配客户端 - 以PSUBSCRIBE订阅的客户端)
    • PUBSUB NUMSUB [pattern]:当前普通订阅客户端数(不含模式订阅匹配客户端)
    • PUBSUB NUMPAT: 查看模式订阅客户端数量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
127.0.0.1:6379> PUBSUB CHANNELS f*
1) "fo"
127.0.0.1:6379> PUBSUB CHANNELS
1) "fo"
2) "k"
127.0.0.1:6379> PUBSUB NUMSUB f*
1) "f*"
2) (integer) 0
127.0.0.1:6379> PUBSUB NUMSUB fo
1) "fo"
2) (integer) 1
127.0.0.1:6379> PUBSUB NUMPAT
(integer) 1

3.2. 事务处理

事务中的所有命令都被序列化并按顺序执行,要么处理所有命令,要么不处理任何命令,因此Redis事务也是原子的(但如果事务执行过程中,服务崩溃,以aof方式添加的依旧可能有部分写入的情况,可以基于redis-check-aof修复部分写入内容)

事务用法:

  • MULTI:标记事务开始,阻塞当前连接,子命令将被排队作为原子操作,并直至EXEC
    • 如果在排队命令时出错,命令无法加入队列(比如不存在的命令操作),大多数客户端将中止丢弃该事务的事务;
    • 如果命令排队成功,那么即使命令失败(比如操作不存在的元素LPOP a),队列中的所有其他命令也会被处理;
    • MULTI不能嵌套,否则报错
    • EXEC在没有开启MULTI下,也会报错
    • Redis不支持回滚
  • DISCARD:重置连接为正常,刷新事务队列并退出事务;
    • DISCARD后,不用在执行EXEC,类似于ROLLBACK
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 排队成功,LOP a命令失败
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET a 3
QUEUED
127.0.0.1:6379> LPOP a
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> GET a
"3"
  • WATCH:用于为Redis事务提供检查和设置(CAS Check-AND-SET)行为,进行乐观锁定,适用不太可能发生冲突的场景
    • 使EXEC成为条件的命令,只有在WATCH观察的键没有修改情况下(Check),EXEC才会执行事务(Set)
    • 可以向单个WATCH呼叫发送任意数量的密钥
    • 可以多次调用WATCH
    • 可以使用UNWATCH命令(不带参数)来刷新所有被监视的键​​
    • 当EXEC被调用时,所有按键都UNWATCH
  • Redis脚本是事务性,也可以基于脚本来实现事务
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 多客户端同时执行下述代码,会产生竟态条件问题,导致设置错误的值
val = GET mykey
val = val + 1
SET mykey $val

// 基于CAS设置,如果存在竞争条件,另一个客户端修改了val我们对WATCH的调用和我们对EXEC的调用之间的时间结果,则事务将失败。
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

4. Redis内存回收过程

4.1. 过期键的删除处理

Redis 使用以下两种方式删除过期的键:

  • 当一个键被访问时,程序会对这个键进行检查,如果键已经过期,那么该键将被删除。
  • 底层系统会在后台渐进地查找并删除那些过期的键,从而处理那些已经过期、但是不会被访问到的键。

因此,Redis 产生expired通知的时间为过期键被删除的时候, 而不是键的生存时间变为0的时候(可能不会立即回收)。

4.2. 内存回收策略与最大内存配置关系

Redis使用LRU算法(最近最少使用)来驱逐过期缓存(MC也是采用该缓存过期策略),支持Maxmemory最大内存配置(redis.conf配置),当达到最大内存配置,Redis服务采用不同的策略对内存进行回收,具体情况依赖于应用场景;

常见超过最大设置内存回收策略有:

  • allkeys-lru:基于key的LRU算法进行回收,键可能没有过期(希望部分的子集元素将比其它其它元素被访问的更多,符合热度集中,应用不确定,优先选这款)
  • allkeys-random:所有key被随机回收(符合等概率key)
  • volatile-ttl:仅在已过期的键中,基于TTL最短的最新回收
  • volatile-lru: 仅在已过期的键中,基于LRU算法回收
  • volatile-random: 仅在已过期的键中,随机回收

回收在何时进行?当客户端发送命令时候,Redis服务会检测是否超过最大内存限制,如果超过,启用内存回收策略;

LRU不是全量,而是会有一个采样量,可以通过配置调整每次回收时检查的采样数量:maxmemory-samples 5,使用10个采样大小的Redis 3.0的近似值已经非常接近理论的性能(消耗更多的CPU时间以实现更真实的LRU算法)。2

5. Redis分区

5.1. 分区目的/好处

  1. 提供更大的数据存储空间,可以将一个大的数据集散列到多个子Redis实例中;
  2. 提供更大的吞吐,因为不同机器可以独立提供网卡、CPU、内存资源,以提供服务;

5.2. 分区的理解

假设有RO~R3四个实例,我们存放用户数据user:1~user:n,简单方式有:

  • 按范围分区存储,比如用户1~10k存R0,10k~20k存R1…,弊端:需要映射表关系,而且后续调整不方便,同时还可能有热点问题
  • 按散列分区存储,基于特定的散列函数,比如CRC32得到数值后,比如crc32(foobar)=93024922,在93024922 mod 4=2,存储在R2实例
  • 基于一致性hash,在环形上面基于Hash函数初始化机器节点,针对Key值进行一致性Hash,方便在节点变更时候,缓存可以最少限度的重新调整

5.3. 分区实现(前中后实现)

  • 客户端分区:由客户端直接求得与Rx实例通信
  • 代理分区:代表Twemproxy,客户端与代理进行Redis协议通信,同时代理与真正的Redis实例Rx进行通信
  • 路由查询:将查询发送到随机实例,有实例自行转发到正确的节点(Redis集群就是这个)

5.4. 分区带来的问题

  1. 不支持涉及多个键的操作(比如集合操作,可能已跨实例了)
  2. 涉及多key操作的事务无法使用
  3. 仅Key的粒度,无法针对大Key数据集进行拆分(比如很大的集合数据)
  4. 使用分区,数据被散落到多个分区上面,数据维护更加复杂(必须处理了多份RDB/AOF文件)
  5. 容量不容易调整,数据重新平衡代价比较高(客户端分区和代理分区基本无法支持)

5.5. 数据缓存Or存储

  • 如果使用Redis作为缓存,则使用一致哈希可以轻松扩展和缩小。(比如Predis支持一致性Hash)
  • 如果Redis用作存储,则使用固定的键到节点映射,因此节点数必须固定且不能变化。否则,需要一个能够在添加或删除节点时在节点之间重新平衡密钥的系统,目前只有Redis Cluster能够执行此操作

另外还有可以通过Presharding来折中数据再分配的问题的问题

6. Redis常见应用模式

6.1. 消息队列:FIFO (阻塞和非阻塞)

  • RPUSH+LPOP模式(队尾插入,队首移除)/LPUSH+RPOP模式
  • 队列消费
    • 轮询:耗CPU和服务Redis查询资源
    • 阻塞:BRPOP/BLPOP
1
2
3
4
5
// BLPOP key [key ...] timeout

127.0.0.1:6379> BLPOP mymq 3
(nil)
(3.05s)

Tips:如果直接消费出队列,消息可能会丢失,可以采用RPOPLPUSH放入安全队列处理

6.2. 栈:LIFO

  • RPUSH+RPOP模式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 3是最后插入,也是最新被POP出来的
127.0.0.1:6379> RPUSH mystack 1 2 3
(integer) 3
127.0.0.1:6379> LRANGE mystack 0 -1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> RPOP mystack
"3"
127.0.0.1:6379> RPOP mystack
"2"
127.0.0.1:6379> RPOP mystack
"1"

6.3. 翻页:LRANGE

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 每页5条数据,
127.0.0.1:6379> RPUSH pagedata 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> LRANGE pagedata 0 -1
 1) "1"
 2) "2"
 3) "3"
 4) "4"
 5) "5"
 6) "6"
 7) "7"
 8) "8"
 9) "9"
10) "10"
127.0.0.1:6379> LRANGE pagedata 0 4
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> LRANGE pagedata 5 9
1) "6"
2) "7"
3) "8"
4) "9"
5) "10"

6.4. 时间轴timeline

场景适用:针对获取最新的N条消息内容,比如最新发布的5条帖子

利用队首插入LPUSH,保证最后写入的是最小的内容,同时利用LTRIM保证链表长度为5,然后利用LRANGE提前列表内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
127.0.0.1:6379> LPUSH timeline 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> LRANGE timeline 0 -1
 1) "10"
 2) "9"
 3) "8"
 4) "7"
 5) "6"
 6) "5"
 7) "4"
 8) "3"
 9) "2"
10) "1"
127.0.0.1:6379> LTRIM timeline 0 4
OK
127.0.0.1:6379> LRANGE timeline 0 -1
1) "10"
2) "9"
3) "8"
4) "7"
5) "6"
127.0.0.1:6379> LPUSH timeline 11 12 13
(integer) 8
127.0.0.1:6379> LTRIM timeline 0 4
OK
127.0.0.1:6379> LRANGE timeline 0 4
1) "13"
2) "12"
3) "11"
4) "10"
5) "9"

6.5. 循环队列,循环检服务情况

场景适用:针对需要循环处理的内容,比如监控一批Web服务的状态

可以利用RPOPLPUSH到同一个队列,形成循环队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
127.0.0.1:6379> RPUSH webcheck  www.baidu.com www.alibaba.com www.tmall.com www.qq.com
(integer) 4
127.0.0.1:6379> LRANGE webcheck
(error) ERR wrong number of arguments for 'lrange' command
127.0.0.1:6379> LRANGE webcheck 0 -1
1) "www.baidu.com"
2) "www.alibaba.com"
3) "www.tmall.com"
4) "www.qq.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.qq.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.tmall.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.alibaba.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.baidu.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.qq.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.tmall.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.alibaba.com"
127.0.0.1:6379> RPOPLPUSH webcheck webcheck
"www.baidu.com"

6.6. TOPN排序 - ZSORT

常见电商热销榜,基于一定规则生成热销商品(比如基于浏览量或者销售量),这个值是变化的,使用LIST不灵活,可以考虑使用有序集合,并结合ZREVRANGE提取指定TopN得分的商品或分类

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> ZADD hotsale 1 cate1 2 cate2 3 cate3 4 cate4 5 cate5
(integer) 5
127.0.0.1:6379> ZREVRANGE hotsale 0 2 withscores
1) "cate5"
2) "5"
3) "cate4"
4) "4"
5) "cate3"
6) "3"

6.7. 随机取值 - 扑克牌游戏(SADD+SPOP)

批量导入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化
redis-cli --pipe|cat
SADD deck c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 cj cq ck 
SADD deck d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 dj dq dk 
SADD deck h1 h2 h3 h4 h5 h6 h7 h8 h9 h10 hj hq hk 
SADD deck S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK
// 输出牌内容(4方,每一方13张,没有鬼)
127.0.0.1:6379> SPOP deck 13
 1) "S6"
 2) "S4"
 3) "C8"
 4) "H10"
 5) "C10"
 6) "S2"
 7) "D5"
 8) "D8"
 9) "D1"
10) "HK"
11) "HJ"
12) "H6"
13) "H4"
...