Redis Introduction And Features

Posted by Jack on 2023-10-14
Words 6.7k and Reading Time 28 Minutes
Viewed Times

Installation

  1. download for linux: https://redis.io/download/
  2. download for windows: https://github.com/tporadowski/redis/releases
  3. reference: https://www.runoob.com/redis/redis-install.html
  4. binary files:
filename description
redis-server 启动redis服务(默认端口6379)
redis-cli 启动redis命令行客户端
redis-benchmark 基准测试工具
redis-check-aof AOF持久化文件检测和修复工具
redis-check-rdb RDB持久化文件检测和修复工具
redis-sentinel 启动哨兵
redis-trib cluster集群构建工具(Redis 3-4 version)

Redis Concepts

  1. Redis(Remote Dictionary Server)是一个开源的基于键值对(key-value)的NoSQL数据库,使用C语言编写,支持网络,基于内存但支持持久化
  2. Redis可以将内存的数据利用快照日志的形式保存到硬盘上,以防断电或机器故障导致数据丢失。Redis还提供键过期(老化)、发布订阅、事务、流水线、Lua脚本等附加功能。
  3. 应用场景:缓存、排行榜系统、计数器应用、社交网络、消息队列系统。

Redis Features

  • 速度快(10W/s)
  • 键值对的数据结构服务器
  • 丰富的功能
  • 简单稳定
  • 持久化
  • 主从复制
  • 高可用和分布式转移
  • 客户端语言多(C/python/java/php)

Redis Persistence

Redis官方提供了两种数据持久化的方式,分别是:RDB(Redis DataBase)和AOF(Append Only File)。

RDB - 保存快照数据

1. 基本原理

RDB持久化主要是通过SAVE和BGSAVE两个命令对Redis数据库中当前的数据做snapshot并生成rdb文件来实现的。这是一个根据服务器配置信息的自动间隔保存操作,其中SAVE是阻塞的,BGSAVE是非阻塞的,后者通过fork一个子进程来完成的。在Redis启动的时候会检测rdb文件,然后载入rdb文件中未过期的数据到服务器中。

2. 实现方式

服务器通过维护dirty计数器lastsave属性分别记录距离上次成功执行SAVE或者BGSAVE命令之后,服务器对数据库状态进行修改的次数和最后一次成功SAVE或者BGSAVE的UNIX时间戳。由Redis周期性操作函数serverCron默认每隔100ms来检测是否满足配置信息中的要求,然后再决定是否执行SAVE或者BGSAVE命令来对数据库进行备份。

3. SAVE与BGSAVE命令的区别

命令 SAVE BGSAVE
IO类型 同步 异步
阻塞客户端 否(阻塞发生在fork进程)
复杂度 O(N) O(N)
优点 不会消耗额外内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要fork消耗内存

4. RDB的优势和劣势

  • 优势:
    a. RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
    b. 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
    c. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  • 劣势:
    RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据

AOF - 记录操作日志

1. 持久化原理

  • AOF持久化是通过存储每次执行的客户端命令,然后由一个伪客户端来执行这些命令将数据写入到服务器中的方式实现的。一共分为命令追加(append)、文件写入、文件同步(sync)三个步骤完成的。
    a. 命令追加:当有修改删除操作时,服务器会在执行完之后以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
    b. 文件写入:Redis的服务进程就是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求。服务器在处理文件事件时可能会执行写命令,同时会追加到aof_buf缓冲区,所以在每结束一次循环之前,都会调用flushAppendOnlyFile函数,将aof_buf缓冲区的数据写入到AOF文件里面。
    c. 文件同步:flushAppendOnlyFile函数通过服务器配置appendfsync选项的值来决定的将每次循环结束之前aof_buf缓冲区的数据写入到AOF文件后,将以何种方式同步到AOF文件里面。

2. 文件重写原理

  • AOF持久化记录的是每条写命令,随着时间推移AOF文件会越来越大,文件重写就是为了解决这个问题。aof_rewrite函数fork一个子进程创建AOF重写缓冲区,将Redis中所有的数据生成多条写命令写入AOF文件。
  • 在子进程进行AOF重写期间,服务器还会处理写请求的命令,这会导致服务器当前的数据库状态和重写后的AOF文件所保存的数据不一致。为了解决这个问题,子进程在执行AOF重写期间,服务器进程需要执行以下三件事情:
    a. 执行客户端发送来的命令
    b. 将执行后的写命令追加到AOF缓冲区
    c. 将执行后的写命令追加到AOF重写缓冲区
  • 当子进程完成AOF重写工作后,会发送一个信号到父进程,父进程收到信号后会调用信号处理函数(这个过程会block主父进程),执行以下工作:
    a. 将AOF重写缓冲区中的数据全部写入到新AOF文件中,这时新AOF文件所保存的数据库状态和服务器当前的数据库状态一致
    b. 对新的AOF文件进行改名,原子的覆盖现有的AOF文件,完成新旧两个AOF文件的替换

3. AOF触发机制

  • always:同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好。
  • everysec:异步操作,每秒记录,如果一秒内宕机,有数据丢失。
  • no:从不同步。
命令 always everysec no
优点 不丢失数据 每秒全量同步 不用管
缺点 IO开销较大,一般sata盘只有几百tps 丢1秒的数据 不可控

4. AOF的优势与劣势

  • 优势:
    a. AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
    b. AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
    c. AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
    d. AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复
  • 劣势:
    a. 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
    b. AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然每秒一次fsync,性能也还是很高的。
    c. AOF曾经发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

RDB与AOF的区别

  • RDB可以理解为是一种全量数据更新机制,AOF可以理解为是一种增量的更新机制,AOF重写可以理解为是一种全量+增量的更新机制(第一次是全量,后面都是增量)。
  • RDB适合服务器数据库数据量小,写命令频繁的场景。AOF则适合数据量大,写命令少的场景。
模式 RDB AOF
启动优先级
体积
恢复速度
数据安全性 丢数据 根据策略决定
轻重

Redis Global Command

  • 设置/获取键(支持批量操作)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    127.0.0.1:6379> set name jack
    OK
    127.0.0.1:6379> get name
    "jack"
    127.0.0.1:6379> mset age 24 height 180
    OK
    127.0.0.1:6379> mget age height
    1) "24"
    2) "180"
    127.0.0.1:6379> set test hello ex 100
    OK # ex/px表示同时设置过期时间s/ms,ex可等同于setex test 100 hello
    127.0.0.1:6379> set test hello nx
    OK # nx表示键不存在才允许设置,等同于setnx test hello
    127.0.0.1:6379> set test1 hello xx
    (nil) # xx表示键存在才允许设置,即覆盖
  • 查看所有键(支持模糊匹配)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    127.0.0.1:6379> keys *
    1) "age"
    2) "height"
    3) "name"
    127.0.0.1:6379> keys a*
    1) "age"
    127.0.0.1:6379> keys *a*
    1) "age"
    2) "name"
  • 统计键总数
    1
    2
    127.0.0.1:6379> dbsize
    (integer) 3
  • 检查键是否存在
    1
    2
    3
    4
    127.0.0.1:6379> exists test
    (integer) 0
    127.0.0.1:6379> exists name
    (integer) 1
  • 设置键过期
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    127.0.0.1:6379> expire height 20
    (integer) 1
    127.0.0.1:6379> ttl height
    (integer) 13
    127.0.0.1:6379> ttl height
    (integer) 5
    127.0.0.1:6379> ttl height
    (integer) -2 # -2表示已过期
    127.0.0.1:6379> ttl age
    (integer) -1 # -1表示永不过期
    127.0.0.1:6379> expireat key timestamp
    # 设置键在指定时间戳后过期

    127.0.0.1:6379> set test hhh ex 10000
    OK
    127.0.0.1:6379> ttl test
    (integer) 9997
    127.0.0.1:6379> pttl test
    (integer) 9992873 # ms级
    127.0.0.1:6379>
    127.0.0.1:6379> persist test
    (integer) 1 # 去除过期时间
    127.0.0.1:6379> ttl test
    (integer) -1
  • 查看键类型
    1
    2
    3
    4
    127.0.0.1:6379> type age
    string
    127.0.0.1:6379> type name
    string
  • 删除键(支持删除多个键)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    127.0.0.1:6379> mset you hello me world
    OK
    127.0.0.1:6379> keys *
    1) "you"
    2) "age"
    3) "me"
    4) "name"
    127.0.0.1:6379> del you me
    (integer) 2
    127.0.0.1:6379> keys *
    1) "age"
    2) "name"
  • 其他操作
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    127.0.0.1:6379> keys *
    1) "test"
    2) "age"
    3) "name"
    127.0.0.1:6379> randomkey
    "name" # 随机获取一个key
    127.0.0.1:6379> randomkey
    "test"
    127.0.0.1:6379> rename test good
    OK # 重命名键,直接覆盖
    127.0.0.1:6379> get test
    (nil)
    127.0.0.1:6379> get good
    "hhh"
    127.0.0.1:6379> renamenx good age
    (integer) 0 # 重命名键,新名称存在则不会生效
    127.0.0.1:6379> renamenx good bad
    (integer) 1
    127.0.0.1:6379> get good
    (nil)
    127.0.0.1:6379> get bad
    "hhh"
    127.0.0.1:6379> get age
    "24"
    127.0.0.1:6379> incr age
    (integer) 25 # 整数自增1
    127.0.0.1:6379> incrby age 5
    (integer) 30 # 整数增加5
    127.0.0.1:6379> decr age
    (integer) 29 # 整数自减1
    127.0.0.1:6379> decrby age 3
    (integer) 26 # 整数减少3
    127.0.0.1:6379> append name " snow"
    (integer) 9 # 字符串追加
    127.0.0.1:6379> get name
    "jack snow"
    127.0.0.1:6379> strlen name
    (integer) 9 # 获取字符串长度
    127.0.0.1:6379> getrange name 0 4
    "jack " # 获取子串,闭区间
    127.0.0.1:6379> getrange name -4 -1
    "snow" # 索引可用负数
    127.0.0.1:6379> setrange name 4 nope
    (integer) 9 # 从指定位置设置
    127.0.0.1:6379> get name
    "jacknopew"

Data Structure

Hash

哈希是一个string类型的field和value的映射表,适用于存储对象(复杂的数据结构),合理使用可减少内存空间消耗(相比于单独存储对象每个属性的字符串或直接存储对象序列化后的字符串)。下面的操作中hmsethmgethkeyshvalshgetall的时间复杂度为O(N),其余操作的时间复杂度为O(1)。

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> hset user:1 name mike
(integer) 1 # 设置field值
127.0.0.1:6379> hget user:1 name
"mike" # 获取field值
127.0.0.1:6379> hmset user:1 age 24 weight 150
OK # 批量设置值
127.0.0.1:6379> hmget user:1 name age weight
1) "mike" # 批量获取值
2) "24"
3) "150"
127.0.0.1:6379> hdel user:1 weight
(integer) 1 # 删除field值
127.0.0.1:6379> hexists user:1 weight
(integer) 0 # 判断field是否存在
127.0.0.1:6379> hkeys user:1
1) "name" # 获取所有的fields
2) "age"
127.0.0.1:6379> hvals user:1
1) "mike" # 获取所有的values
2) "24"
127.0.0.1:6379> hgetall user:1
1) "name" # 获取所有fields和values,若是太复杂的数据结构则可能导致阻塞
2) "mike"
3) "age"
4) "24"
5) "weight"
6) "150"
127.0.0.1:6379> hincrby user:1 age 5
(integer) 29 # field增加5
127.0.0.1:6379> hlen user:1
(integer) 2 # 统计field数量

List

列表用来存储多个有序的字符串,一个列表最多可以存储232 - 1个元素。通过索引下标来获取元素或者子列表,元素可以重复。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
127.0.0.1:6379> lpush nums 1
(integer) 1 # 从nums列表左边插入1(没有则自动创建列表)
127.0.0.1:6379> rpush nums 2 3
(integer) 3 # 从nums列表右边插入2、3
127.0.0.1:6379> lrange nums 0 -1
1) "1" # 获取nums列表索引0到-1的元素(-1表示最后一个下标)
2) "2"
3) "3"
127.0.0.1:6379> linsert nums before 1 -1
(integer) 4 # 在nums列表指定元素前面插入元素
127.0.0.1:6379> lrange nums 0 -1
1) "-1"
2) "1"
3) "2"
4) "3"
127.0.0.1:6379> linsert nums after 2 -2
(integer) 5 # 在nums列表指定元素后面插入元素
127.0.0.1:6379> lrange nums 0 -1
1) "-1"
2) "1"
3) "2"
4) "-2"
5) "3"
127.0.0.1:6379> llen nums
(integer) 5 # 获取nums列表大小
127.0.0.1:6379> lpop nums
"-1" # 从nums最左边删除元素
127.0.0.1:6379> rpop nums
"3" # 从nums最右边删除元素
127.0.0.1:6379> lrange nums 0 -1
1) "1"
2) "2"
3) "-2"
127.0.0.1:6379> lpush nums 1 1 1
(integer) 6
127.0.0.1:6379> lrange nums 0 -1
1) "1"
2) "1"
3) "1"
4) "1"
5) "2"
6) "-2"
127.0.0.1:6379> lrem nums 3 1
(integer) 3 # 删除列表中3个为1的元素(正数表示从左往右删)
127.0.0.1:6379> lrange nums 0 -1
1) "1"
2) "2"
3) "-2"
127.0.0.1:6379> lrem nums -1 2
(integer) 1 # 删除列表中1个为2的元素(负数表示从右往左删)
127.0.0.1:6379> lrange nums 0 -1
1) "1"
2) "-2"
127.0.0.1:6379> lrem nums 0 1
(integer) 1 # 删除列表中所有为1的元素(0表示删除所有)
127.0.0.1:6379> lrange nums 0 -1
1) "-2"
127.0.0.1:6379> lindex nums 0
"-2" # 获取指定下标0的元素
127.0.0.1:6379> lset nums 0 10
OK # 修改指定下标0的元素值
127.0.0.1:6379> lindex nums 0
"10"
127.0.0.1:6379> lpush nums 20 30 40
(integer) 4
127.0.0.1:6379> lrange nums 0 -1
1) "40"
2) "30"
3) "20"
4) "10"
127.0.0.1:6379> blpop nums 0 20
1) "nums" # 从左阻塞删除指定下标元素(超时20)
2) "40"
127.0.0.1:6379> lrange nums 0 -1
1) "30"
2) "20"
3) "10"
127.0.0.1:6379> brpop nums 0 20
1) "nums" # 从右阻塞删除指定下标元素(超时20)
2) "10"
127.0.0.1:6379> lrange nums 0 -1
1) "30"
2) "20"

Set

集合不允许有重复的元素,且是无序的,支持计算交集、并集和差集。集合运算的时间复杂度是O(M+N)。

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
32
33
34
35
36
37
38
39
40
41
42
127.0.0.1:6379> sadd set:1 a b c
(integer) 3 # 往集合中添加元素
127.0.0.1:6379> smembers set:1
1) "b" # 查看集合的所有元素
2) "c"
3) "a"
127.0.0.1:6379> scard set:1
(integer) 3 # 统计集合中元素个数
127.0.0.1:6379> srem set:1 a
(integer) 1 # 移除集合中单个(支持多个)元素
127.0.0.1:6379> sismember set:1 a
(integer) 0 # 判断元素是否在集合中(0-不在,1-在)
127.0.0.1:6379> srandmember set:1 3
1) "b" # 随机获取集合的3个元素(这里实际只有2个)
2) "c"
127.0.0.1:6379> spop set:1 2
1) "b" # 随机弹出2个元素
2) "c"
127.0.0.1:6379> sadd set:1 a b c d
(integer) 4
127.0.0.1:6379> sadd set:2 d e f
(integer) 3
127.0.0.1:6379> sinter set:1 set:2
1) "d" # 多个集合求交集
127.0.0.1:6379> sunion set:1 set:2
1) "d" # 多个集合求并集
2) "b"
3) "f"
4) "c"
5) "a"
6) "e"
127.0.0.1:6379> sdiff set:1 set:2
1) "b" # 多个集合求差集(以第一个为基准)
2) "a"
3) "c"
127.0.0.1:6379> sdiff set:2 set:1
1) "e"
2) "f"
127.0.0.1:6379> sinterstore set:3 set:1 set:2
(integer) 1 # 集合运算结果保存(同理有sunionstore/sdiffstore)
127.0.0.1:6379> smembers set:3
1) "d"

ZSet

有序集合中,插入每个元素要求同时指定一个分数,常用于排行榜,如视频播放量、点赞数等。ZSET、SET以及LIST的区别如下表:

数据结构 是否允许元素重复 是否有序 有序实现方式 应用场景
LIST 索引下标 时间轴,消息队列
SET 标签,社交
ZSET 分值 排行榜,点赞数

有序集合的相关操作例子。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
127.0.0.1:6379> zadd course:1 80 jack 70 snow 85 lily 66 david 58 john
(integer) 5 # 有序集合中添加元素
127.0.0.1:6379> zcard course:1
(integer) 5 # 统计有序集合元素的个数
127.0.0.1:6379> zscore course:1 jack
"80" # 返回某个元素的分数
127.0.0.1:6379> zrank course:1 jack
(integer) 3 # 返回某个元素的升序分数排名
127.0.0.1:6379> zrevrank course:1 jack
(integer) 1 # 返回某个元素的降序分数排名
127.0.0.1:6379> zrange course:1 0 5
1) "john" # 查看升序后的元素
2) "david"
3) "snow"
4) "jack"
5) "lily"
127.0.0.1:6379> zrevrange course:1 0 5
1) "lily" # 查看降序后的元素
2) "jack"
3) "snow"
4) "david"
5) "john"
127.0.0.1:6379> zrem course:1 john
(integer) 1 # 移除某个元素
127.0.0.1:6379> zrange course:1 0 5
1) "david"
2) "snow"
3) "jack"
4) "lily"
127.0.0.1:6379> zincrby course:1 10 jack
"90" # 增加某个元素的分数
127.0.0.1:6379> zrange course:1 0 4 withscores
1) "david" # 查看升序后的元素以及对应分数
2) "66"
3) "snow"
4) "70"
5) "lily"
6) "85"
7) "jack"
8) "90"
127.0.0.1:6379> zrangebyscore course:1 80 99
1) "lily" # 查看某个分数区间的元素
2) "jack"
127.0.0.1:6379> zrangebyscore course:1 80 99 withscores
1) "lily" # 查看某个分数区间的元素以及对应分数
2) "85"
3) "jack"
4) "90"
127.0.0.1:6379> zrangebyscore course:1 (85 99 withscores
1) "jack" # 查看某个分数开区间的元素以及对应分数
2) "90"
127.0.0.1:6379> zcount course:1 (66 99
(integer) 3 # 统计分数在某个开区间范围的元素个数
127.0.0.1:6379> zadd course:2 78 jack 95 lily 88 snow 81 david
(integer) 4
127.0.0.1:6379> zrange course:2 0 4 withscores
1) "jack"
2) "78"
3) "david"
4) "81"
5) "snow"
6) "88"
7) "lily"
8) "95"
127.0.0.1:6379> zrange course:1 0 4 withscores
1) "david"
2) "66"
3) "snow"
4) "70"
5) "lily"
6) "85"
7) "jack"
8) "90"
127.0.0.1:6379> zinterstore course:avg 2 course:1 course:2
(integer) 4 # 有序集合求交集,默认分数直接求和,zinterstore destination numkeys key [key ...]
127.0.0.1:6379> zrange course:avg 0 -1 withscores
1) "david"
2) "147"
3) "snow"
4) "158"
5) "jack"
6) "168"
7) "lily"
8) "180"
127.0.0.1:6379> zinterstore course:avg 2 course:1 course:2 weights 0.2 0.8
(integer) 4 # 有序集合求交集,并指定每个集合中分数的权重,若是求并集使用zunionstore
127.0.0.1:6379> zrange course:avg 0 -1 withscores
1) "david"
2) "78"
3) "jack"
4) "80.400000000000006"
5) "snow"
6) "84.400000000000006"
7) "lily"
8) "93"

有序集合相关操作的时间复杂度:

操作 时间复杂度
zcard/zscore O(1)
zadd/zrem O(klog(n)),k为添加/删除的元素个数,n为集合中的元素个数
zrank/zrevrank/zcount/zincrby O(log(n)),n为集合中的元素个数
zrange/zrangebyscore O(log(n)+k),k为要获取的元素个数,n为集合中的元素个数
zinterstore O(nk)+O(mlog(m)),k是有序集合的个数,n是k个有序集合中最少的元素个数,m是结果集合中的元素个数
zunionstore O(n)+O(m*log(m)),n是所有有序集合元素个数之和,m是结果集合中的元素个数

Redis Application

缓存热点数据

热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存。内存中的数据也提供了AOF和RDB等持久化机制可以选择,要冷、热的还是忽冷忽热的都可选。
在spring后端开发中,常用场景是select 数据库前先查询redis,有的话使用redis数据,放弃select 数据库,没有的话就select 数据库,然后将数据插入redis;
update或者delete数据库前,查询redis是否存在该数据,存在的话先删除redis中数据,然后再update或者delete数据库中的数据。

计数器

由于单线程,可以避免并发问题,保证不会出错,而且100%毫秒级性能。在Redis的数据结构中,stringhashsorted set都提供了incr方法用于原子性的自增操作,下面举例说明一下它们各自的使用场景:

  1. 如果应用需要显示每天的注册用户数,便可以使用string作为计数器,设定一个名为REGISTERED_COUNT_TODAY的 key,并在初始化时给它设置一个到凌晨0点的过期时间,每当用户注册成功后便使用incr命令使该key增长1,同时当每天凌晨0点后,这个计数器都会因为key过期使值清零。
  2. 每条微博都有点赞数、评论数、转发数和浏览数四条属性,这时用hash进行计数会更好,将该计数器的key设为weibo:weibo_id,hash的field为like_number、comment_number、forward_number和view_number,在对应操作后通过hincrby使hash中的field自增。
  3. 如果应用有一个发帖排行榜的功能,便选择sorted set吧,将集合的key设为POST_RANK。当用户发帖后,使用zincrby将该用户id的score增长 1。sorted set会重新进行排序,用户所在排行榜的位置也就会得到实时的更新。

消息队列

Redis 中list的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者/消费者模型)。消息的生产者只需要通过lpush将消息放入list,消费者便可以通过rpop取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择sorted set。而pub/sub功能也可以用作发布者/订阅者模型的消息。无论使用何种方式,由于Redis拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。
List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间:

  1. blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
  2. brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
    上面的操作。

位操作(大数据处理)

  • 用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。
  • 想想一下腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,你能怎么做?这里要用到位操作——使用setbit、getbit、bitcount命令。
  • 原理是:
    redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示我们上面例子里面的用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统,上面我说的几个场景也就能够实现。用到的命令是:setbit、getbit、bitcount。

分布式锁与单线程机制

如今都是分布式的环境下java自带的单体锁已经不适用的。在 Redis 2.6.12 版本开始,string的set命令增加了一些参数:
EX:设置键的过期时间(单位为秒)
PX:设置键的过期时间(单位为毫秒)
NX:只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX:只在键已经存在时,才对键进行设置操作。

由于这个操作是原子性的,可以简单地以此实现一个分布式的锁,例如:

1
set lock_key locked NX EX 1

如果这个操作返回false,说明key的添加不成功,也就是当前有人在占用这把锁。而如果返回true,则说明得了锁,便可以继续进行操作,并且在操作后通过del命令释放掉锁。并且即使程序因为某些原因并没有释放锁,由于设置了过期时间,该锁也会在 1 秒后自动释放,不会影响到其他程序的运行。

显式最新的项目列表

例如新闻列表页面最新的新闻列表,如果总数量很大的情况下,尽量不要使用select a from A limit 10这种低级表达,尝试redis的 LPUSH命令构建List,一个个顺序都塞进去就可以啦。不过万一内存清掉了咋办?也简单,查询不到存储key的话,用mysql查询并且初始化一个List到redis中就好了。

排行榜

使用sorted set(有序set)和一个计算热度的算法便可以轻松打造一个热度排行榜,zrevrangebyscore可以得到以分数倒序排列的序列,zrank可以得到一个成员在该排行榜的位置(是分数正序排列时的位置,如果要获取倒序排列时的位置需要用zcard-zrank)。
id 为6001 的新闻点击数加1:

1
zincrby hotNews:20190926 1 n6001

获取今天点击最多的15条:
1
zrevrange hotNews:20190926 0 15 withscores

倒序索引

倒排索引是构造搜索功能的最常见方式,在 Redis 中也可以通过set进行建立倒排索引,这里以简单的拼音 + 前缀搜索城市功能举例:
假设一个城市北京,通过拼音词库将北京转为beijing,再通过前缀分词将这两个词分为若干个前缀索引,有:北、北京、b、be…beijin和beijing。将这些索引分别作为set的 key(例如:index:北)并存储北京的 id,倒排索引便建立好了。接下来只需要在搜索时通过关键词取出对应的set并得到其中的 id 即可。


...

...

00:00
00:00