Redis 复习知识点(更新中)

Remote Dictionary Server (远程字典服务)是完全开源的,是一个高性能的 Key-Value 数据库,提供了丰富的数据结构,例如 String、Hash、List、Set、ZSet 等等。

1 Redis 安装和启动

1.1 安装

下载地址:https://redis.io/downloads/

# 安装依赖 gcc 和 tcl
sudo apt install make gcc tcl

tar -zxvf redis-7.0.15.tar.gz
cd redis-7.0.15

# 编译并安装
sudo make && sudo make install

编译安装完成,默认安装目录在 /usr/local/bin 下,该目录默认配置到环境变量,所以可以在任意位置使用该目录下的命令:

  • redis-cli:Redis 提供的命令行客户
  • redis-server:Redis 的服务端启动脚本
  • redis-sentinel:Redis 的哨兵启动脚本
  • redis-check-rdb:修复损坏的 RDB 文件
  • redis-check-aof:检查并修复记录的错误的命令

在 Redis 安装目录 redis-7.0.15 下,修改 redis.conf(配置文件参数解释见 1.3):

bind 0.0.0.0

protected-mode no

daemonize yes

port 6379

dir .

databases 16

maxmemory 512mb

logfile 'redis.log'

1.2 启动

修改完配置文件后,启动 Redis:

redis-server redis.conf

ps -ef | grep redis

进入 Redis 命令:

redis-cli

1.3 Redis 配置文件

以下是 Redis7 配置文件示例:

############################## 网络配置 ##############################

# 只允许指定网卡的 Redis 请求
# 如没有指定或者指定 0.0.0.0,则可以接受来自任意一个网卡的 Redis 请求
bind 0.0 . 0.0

# 是否开启保护模式。默认 yes 表示 Redis 只允许本地访问,拒绝外部访问,一般修改为 no
protected -mode no

# Redis 监听端口号,默认为 6379
port 6379

# 设置 TCP 的 backlog 连接队列。在高并发情况下,调大该值
tcp-backlog 511

# 连接超时时间(单位秒),连接超过超时时间,服务端会断开连接
# 设置为 0,则服务端不会主动断开连接
timeout 0

# TCP 连接保活策略(单位秒),每隔 300 秒对空闲客户端发送 ACK 请求
tcp-keepalive 300

############################## 常规配置 ##############################

# 是否在后台执行。yes 表示后台运行;no 表示不是后台运行
# 注意该设置与 docker -d 启动命令冲突,如果用 docker 启动要设为 no  
daemonize yes

# Redis 以后台运行(daemonize 为 yes)时,Redis 进程文件写入位置,默认 /var/run/redis.pid
pidfile /var/run/redis.pid

# 指定日志级别  # 1. debug(大量信息,对开发测试有用)   # 2. verbose(冗余信息,但是没有 debug 乱)   # 3. notice(生产环境使用)   # 4. warning(只记录非常重要的信息)
loglevel notice

# 指定日志文件路径
logfile ""

# 指定数据库数量
databases 16

############################## 快照配置 ##############################

# 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件  # 900秒(15分钟)内至少 1 个 key 值改变(则进行持久化)   # 300秒(5分钟)内至少 10 个 key 值改变(则进行持久化)   # 60秒(1分钟)内至少 10000 个 key 值改变(则进行持久化)
save 900 1
save 300 10
save 60 10000

# 当 RDB 持久化出现错误后,是否依然进行继续进行工作,yes 表示不能进行工作,no 表示可以继续进行工作
stop-writes-on-bgsave-error yes

# 是否压缩 RDB 文件(需要消耗一点 CPU 资源)  rdbcompression yes

# 保存 RDB 文件时是否进行错误检验
rdbchecksum yes

# RDB 文件名
dbfilename dump.rdb

# RDB 文件的保存目录,默认 ./ 表示当前目录
dir ./

############################## 主从复制配置 ##############################

# 副本需要添加的配置,指定其连接的主节点 ip 和 port
# replicaof <master ip> <master port>
 
# 如果主节点设置了密码(即其配置了 requirepass)
# 则副本必须在此配置主节点的密码,完成授权才能连接主节点
# masterauth <master-password>

# 副本与主节点失联后,是否继续为客户端提供服务
# 设置为 yes,副本继续为客户端提供只读服务,有可能是过期数据
# 设置为 no,除了 INFO、REPLICAOF、AUTH 等命令,任何向此副本发送的请求都会被告知 error
replica-serve-stale-data yes

# 副本是否只读
replica-read-only yes

# 配置主节点向副本发送心跳的时间间隔,单位秒
# repl-ping-replica-period 10

# 主从数据同步是否使用无磁盘同步功能
# 主节点直接创建一个新进程将 RDB 文件写入副本的 socket 中
# 推荐在磁盘性能差,网络性能好时使用
repl-diskless-sync yes

# 当启用无磁盘同步时,设置服务器等待的延时
# 一旦 RDB 文件传输开始,就不能为新到的副本提供服务。这些副本将排队等待下一次 RDB 传输
# 设置该延时可以让更多的副本到达,一次性进行传输
# 延迟时间以秒为单位,默认为 5 秒。设置为 0 表示禁用
repl-diskless-sync-delay 5

# 当启用延时无磁盘同步时,如果连接的副本达到以下设置的最大数量,则在副本达到最大数量启动同步,而无需等待上面设置的延时
# 默认值为 0 表示不设置副本最大值,每次等待 5 秒后进行同步
repl-diskless-sync-max-replicas 0

# 不使用无磁盘加载,首先将 RDB 文件存储到磁盘
repl-diskless-load disabled

# 同步后是否在副本的 socket 中禁用 TCP_NODELAY
# yes 表示 Redis 将使用更少的 TCP 数据包和更少的带宽向副本发送数据,但这可能会增加副本的数据延迟
# no 表示副本的数据延迟将减少,但将使用更多的带宽
repl-disable-tcp-nodelay no

# 配置副本优先级,Redis 哨兵使用该值选择一个副本在主节点挂掉后将其晋升为主节点
# 优先级越低的副本晋升为主节点的概率越大(除了优先级为 0 的副本永远不会晋升)
replica-priority 100

############################## 安全配置 ##############################

# 配置 ACL 日志的最大记录长度
# ACL(Access Control List)即访问控制列表,可以控制客户端对于不同 Redis 命令和数据的访问权限
# ACL 日志可以对 ACL 阻止的失败命令进行故障排除。例如,认证失败的连接尝试,或者访问了当前 ACL 规则不允许的 key 的命令
acllog-max-len 128

# 配置密码
# requirepass foobared

############################## 客户端配置 ##############################

# 配置最大连接数,默认 10000
# maxclients 10000

############################## 内存管理配置 ##############################

# 配置内存使用限制为指定的最大字节数
# 当达到内存限制时,Redis 根据选择的 maxmemory-policy 尝试删除 key
# maxmemory <bytes>

# 内存达到限制后的处理策略
# 1. volatile-lru -> 使用 LRU 算法(最近最少使用)删除 key,作用于设置了过期时间的 key
# 2. allkeys-lru -> 使用 LRU 算法(最近最少使用)删除 key,作用于所有 key
# 3. volatile-lfu -> 使用 LFU 算法(最少使用)删除 key,作用于设置了过期时间的 key
# 4. allkeys-lfu -> 使用 LFU 算法(最少使用)删除 key,作用于所有 key
# 5. volatile-random -> 随机删除 key,作用于设置了过期时间的 key
# 6. allkeys-random -> 随机删除 key,作用于所有 key
# 7. volatile-ttl -> 删除最快要过期的 key,作用于设置了过期时间的 key
# 8. noeviction -> 不会删除任何 key,只会在写操作时返回 error
# maxmemory-policy noeviction

############################## 惰性删除配置 ##############################

# 当删除 key 时,Redis 提供异步惰性删除 key 内存的功能,即把 key 释放操作放在 BIO 单独的子线程中处理,减少删除大 key 对主线程的阻塞

# 配置删除 key 时是否开启惰性删除
lazyfree-lazy-eviction no

# 配置删除过期 key 时是否开启惰性删除
lazyfree-lazy-expire no

# 配置执行某些指令时(例如 rename)是否开启惰性删除
lazyfree-lazy-server-del no

# 配置主从同步时是否开启惰性删除
lazyfree-lazy-flush no

# 配置用户手动删除 key 时是否开启惰性删除
lazyfree-lazy-user-del no

# 配置用户手动 flushdb 时是否开启惰性删除
lazyfree-lazy-user-flush no

############################## 线程 IO 配置 ##############################

# 配置 Redis 扩展 IO 数量,默认禁用或者设置为 1 表示主线程模式
# 官方建议 4 核及 4 核以上的机器启用,而建议保留一个核。例如 4 核的机器使用 2-3 个 IO 线程,8 核机器使用 6 个 IO 线程
# 超过 8 个线程没有太大的意义,只有遇到了性能问题才开启这部分
# io-threads 4

# 如果配置了 1 个以上的线程数量,启用的 IO 线程只进行写操作
# 如果希望启用的 IO 线程能进行读操作和协议解析,则设置为 yes
# io-threads-do-reads no

############################## 内核 OOM 控制配置 ##############################

# 默认配置,在所有其它子进程之前杀死后台子进程,在主进程之前杀死副本
oom-score-adj no

# 指定主进程、副本进程、后台子进程的值,值越高越可能被杀死
oom-score-adj-value 0 200 800

############################## AOF 配置 ##############################

# 是否开启 AOF 持久化,默认不开启,AOF 对性能有一定损耗,生产环境不开启
appendonly no

# 配置 AOF 文件名
appendfilename "appendonly.aof"

# 配置 AOF 文件存放目录
appenddirname "appendonlydir"

# 配置 AOF 持久化同步策略
# 1. everysec -> 每秒执行一次,发送异常时可能会丢失最后一秒的数据,默认
# 2. always -> 每次写操作执行一次,数据最安全,但是对性能影响最大
# 3. no -> 不主动刷盘,由内核决定什么时候刷盘,数据最不安全,但性能最好
appendfsync everysec

# 配置在重写时是否持久化 AOF 文件,no 表示在重写期间对新的写操作不进行 AOF 持久化,暂存在内存中
no-appendfsync-on-rewrite no

# 配置当前 AOF 文件超过上次多少百分比时,进行重写。默认设置为原文件两倍大
auto-aof-rewrite-percentage 100

# 配置当前 AOF 文件超过多大时,进行重写
auto-aof-rewrite-min-size 64mb

# 配置 AOF 文件末尾截断时的处理策略。在 Redis 意外宕机时可能会导致 AOF 文件被截断
# yes 表示 Redis 重启后仍加载截断的 AOF 文件,并且向客户端发送日志通知该事件
# no 表示 Redis 重启时拒绝启动,用户需要在重启服务之前使用 redis-check-aof 命令修复 AOF 文件
aof-load-truncated yes

# 是否开启混合持久化(AOF +RDB)
aof-use-rdb-preamble yes

############################## 集群配置 ##############################

# 是否开启集群模式
# cluster-enabled yes

# 集群配置文件,该文件 Redis 会自动创建,只需要打开修饰即可
# 每个 Redis 集群节点都需要一个不同的集群配置文件,确保在同一系统中运行的实例没有重复的集群配置文件名
# cluster-config-file nodes-6379.conf

# 配置集群节点表示时间,单位毫秒
# 在集群模式下,主节点之间会发送心跳来检测存活状态,超过该配置时间没有响应的节点被认为宕机
# cluster-node-timeout 15000

# 配置集群端口,设置为默认值 0 表示集群端口为命令端口 + 10000
# cluster-port 0

# 配置副本有效因子,该值决定了主节点宕机后,如何选择一个副本进行故障转移,晋升为主节点
# 如果副本与主节点上次交互的时间大于 (cluster-node-timeout * cluster-replica-validity-factor)+  repl-ping-replica-period,该副本就不会进行故障转移,晋升成为主节点
# 如果设置为 0,则不管该副本和主节点断开多久,都有资格晋升为主节点
# cluster-replica-validity-factorcluster-replica-validity-factor 10

# 配置主节点故障转移时保留的最小副本数,该配置防止出现副本都宕机导致有主节点下面没有副本的情况
# cluster-migration-barrier 1

# 配置是否允许其它主节点的副本迁移到无副本的主节点下面,默认 yes
# cluster-allow-replica-migration yes

# 配置当集群发现有未分配的哈希槽时,集群是否停止查询操作变为不可用
# 一旦所有槽位被覆盖,集群自动恢复可用
# cluster-require-full-coverage yes

# 配置当主节点故障时,是否禁止该副本尝试进行故障转移,默认 no 允许副本进行故障转移
# cluster-replica-no-failover no

# 配置是否允许集群不可用时,副本处理读请求,默认 no
# cluster-allow-reads-when-down no

# 配置是否允许集群不可用时,pub/sub 正常进行,默认 yes
# cluster-allow-pubsubshard-when-down yes

############################## 慢日志配置 ##############################

# 配置命令执行时间超过该值就被记录到慢日志中,单位微秒
slowlog-log-slower-then 10000

# 配置慢日志最大条数,超过该长度的旧记录被删除
slowlog-max-len 128

############################## 调优配置 ##############################

# hash 元素长度不超过 hash-max-listpack-value,且数量不超过 hash-max-listpack-entries 时,数据存储为压缩列表 listpack,否则存储为哈希表
hash-max-listpack-entries 512
hash-max-listpack-value 64

# 列表也以一种特殊的方式编码,节省大量空间。
# 1. -5 -> 最大 64kb,不建议用于正常工作负载
# 2. -4 -> 最大 32kb,不建议
# 3. -3 -> 最大 16kb,可能不推荐
# 4. -2 -> 最大 8kb,推荐
# 5. -1 -> 最大 4kb,推荐
list-max-listpack-size -2

# 列表可以被压缩,但是默认禁用列表压缩
list-compress-depth 0

# set 元素数量不超过 set-max-intset-entries 时,数据存储为压缩列表 intset,否则存储为哈希表
set-max-intset-entries 512

# zset 元素长度不超过 zset-max-listpack-value,且数量不超过 zset-max-listpack-entries 时,数据存储为压缩列表 listpack,否则存储为 skiplist
zset-max-listpack-entries 128
zset-max-listpack-value 64

# 配置 HyperLogLog 稀疏表示的字节限制。当使用稀疏结构的 HyperLogLog 超过此限制时,它将转换为密集结构
# 建议的值是 3000。当 CPU 够用时,但空间不够的情况下,并且数据集由许多基数在 0-15000 范围内的 HyperLogLog 组成时,可以考虑设置为 10000
hll-sparse-max-bytes 3000

# 默认情况下,使用 CPU 每 100 毫秒中的 1 毫秒进行 rehash,在可能的情况下释放内存
activerehashing yes

# 客户端输出缓冲区限制可用于强制断开由于某些原因而没有足够快地从服务器读取数据的客户端(一个常见的原因是 Pub/Sub 客户端无法像发布者生成消息那样快速地使用消息)
# 可以为三种不同类别的客户端设置不同的限制:
# 1. normal -> 普通客户端
# 2. replica -> 副本(slave)
# 3. pubsub -> 订阅了至少一个 pubsub 频道或模式的客户端
# 每个客户端输出缓冲区限制指令的语法如下:
# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
# 一旦达到 hard limit 限制或者达到 soft limit 之后又过了 soft seconds 秒,那么客户端会立即被断开连接
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

# 配置 Redis 执行后台定时任务的频率
# Redis 调用一个内部函数来执行许多后台任务,比如在超时时关闭客户端的连接,清除从未请求过的过期密钥等等。并非所有任务都以相同的频率执行
# 但 Redis 会根据指定的 hz 值调用要执行的任务。默认情况下,hz 设置为 10
# 该范围在 1 到 500 之间,但是超过 100 的值通常不是一个好主意。大多数用户应该使用默认值 10,并且只有在需要非常低延迟的环境中才将其提高到 100
hz 10

# 配置是否开启自适应 hz,使 hz 值与连接客户端数量成比例
dynamic-hz yes

# 当重写 AOF 文件时,如果启用以下选项,则每生成 4MB 的数据,就会对文件进行 fsync。这对于更增量地将文件提交到磁盘并避免大的延迟峰值非常有用
aof-rewrite-incremental-fsync yes

# 当保存 RDB 文件时,如果启用以下选项,则每生成 4MB 的数据就会对该文件进行 fsync。这对于更增量地将文件提交到磁盘并避免大的延迟峰值非常有用
rdb-save-incremental-fsync yes

########## 内存碎片整理配置 ##########

# Jemalloc 后台线程清除将默认启用
jemalloc-bg-thread yes

2 Redis 数据类型及常用命令

Redis 命令参考:http://doc.redisfans.com/。

Redis 是 k-v 键值对进行存储,这里的数据类型是 value 的数据类型,key 的类型都是字符串。

命令说明
keys *获取当前库的所有 key(生产环境使用该命令 CPU 飙升,禁止使用)
exists key判断某个 key 是否存在,返回 1 表示存在,返回 0 表示不存在
type key查看 key 的类型
del key删除指定的 key 数据,返回 1 删除成功。使用 del 删除 key 会产生阻塞
unlink key非阻塞删除,仅仅将 key 从 keyspace 元数据中删除,真正的删除会在后续异步操作
ttl key查看 key 还有多少秒过期,-1 表示永不过期,-2 表示已过期
expire key [秒数]给 key 设置过期时间,以秒为单位
dbsize查看当前数据库 key 的数量
flushdb清空当前库,但不执行持久化(生产环境禁止使用,可能引发缓存雪崩)
flushall清空当前库,并执行持久化(生产环境禁止使用,可能引发缓存雪崩)
move key [0-15]将当前数据库的 key 移动到指定索引的数据库中
select [0-15]切换到数据库 [0-15]

2.1 String

String 是 Redis 最基本的类型,一个 key 对应一个 value,一个 Redis 中字符串 value 最多可以是 512M。

String 底层数据结构是简单动态字符串(Simple Dynamic String, SDS),不仅能存储字符串还能存储整数值,并且具有预分配空间、避免多次内存重分配等优化特性。

String 是二进制安全的,Redis 的 String 可以包含任何数据,比如 jpg 图片或者序列化的对象。

set key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | KEEPTTL]

命令说明
mset key value [key value …]同时设置多个键值对
mget key [key …]同时获取多个 key 的值
msetnx key value [key value …]同时设置多个键值对,必须保证 key 都不存在才能成功
setrange key offset value将 key 的值从字符串下标 offset 处开始设置为 value,字符串下标从 0 开始
substr / getrange key start end获取 key 从字符串下标 start 到 end 的 value
getset key value设置 key 的值为 value ,并返回 key 的旧值
getdel key获取 key 的值,并将其删除
strlen key获取 key 的 value 值的长度
append key value在 key 的字符串值后面拼接 value
lcs key1 key2返回 key1 和 key2 的 value 字符串值的最长公共子序列
lcs key1 key2 len返回最长公共子序列的长度

仅当 key 的 value 值为数字时才能使用以下命令,否则报错:

命令说明
incr keykey 的 value 值 + 1
incrby key incrementkey 的 value 数值 + increment,increment 必须是整数
decr keykey 的 value 数值 - 1
decrby key decrementkey 的 value 数值 -decrement
incrbyfloat key incrementkey 的 value 数值 + increment,increment 可以是整数和小数

2.2 List

当 list 中元素数量时,底层使用压缩列表 ziplist 节省空间。随着元素数量的增加或元素大小超过一定限制,底层使用双向链表 quicklist。

命令说明
lpush / rpush key element [element …]往 key 的值列表左边 / 右边添加 element,越后面的 element 添加在最外面,并返回列表现有的元素个数
lpop / rpop key [count]key 的值列表最左边 / 最右边的 count 个元素弹出列表,并返回这些元素
lpushx / rpushx key element [element …]原理和 lpush 和 rpush 一样,但是如果 key 不存在,就不会执行此操作
blpop / brpop key timeout移除 key 的值列表的第一个元素 / 最后一个元素,如果列表没有元素会阻塞直到 timeout 时间或发现可弹出元素
lrange key start stop返回 key 的值列表从下标 start 到 stop 的元素,start 为 0、stop 为 -1 表示遍历列表全部元素
lindex key index返回 key 的值列表下标为 index 的元素
lpos key element返回 key 的值列表中从左往右第一个匹配的 element 元素的下标
lpos key element [RANK rank]返回 key 的值列表中从左往右第 rank 个匹配的 element 元素的下标,rank 为负数则从右往左匹配
lpos key element [COUNT count]返回 key 的值列表中从左往右前 count 个匹配的 element 元素的下标集合
lpos key element [MAXLEN len]从 key 的值列表中前 len 个元素中查找匹配的 element 元素的下标
llen key返回 key 的值列表元素个数
lrem key count elementkey 的值列表删除 count 个值为 element 的元素,count > 0 表示从左往右删除 count 个值为 element 的元素,count < 0 表示从右往左删除
ltrim key start stopkey 的值列表截取保留下标 start 到 stop 的元素,成功返回 ok
lmove source destination leftright left
blmove source destination leftright left
lset key index elementkey 的值列表下标 index 的元素改为 element,成功返回 ok
linsert key beforeafter pivot element

2.3 Hash

Hash 是一个 String 类型的 field 和 value 的映射,特别适合用于存储对象。

命令说明
hset key field1 value1 [field2 value2 …]设置 key 的 field-value 键值对
hsetnx key field value原理与 hset 一样,不存在赋值,已存在赋值无效
hget key field返回 key 的 field 的 value 元素
hmget key field [field …]返回 key 的给定所有 field 的 value 元素
hgetall key返回 key 的所有 field-value 键值对
hdel key field [field …]删除 key 的给定所有 filed 的 value 值,删除成功返回 1,删除失败返回 0
hlen key返回 key 的 field 数量
hkeys key返回 key 的所有 field
hvals key返回 key 的所有 value
hexists key field返回 key 中是否有 field 这个字段,有返回 1,没有返回 0
hstrlen key field返回 key 的 field 的 value 的长度
hrandfield key [count [withvalues]]随机返回 key 的 field,如果加上 count 指定返回 field 的数量,如果加上 withvalues 返回带上对应的值
hscan key cursor [MATCH pattern] [COUNT count]增量迭代返回 key 的满足匹配的 field 及其值。cursor 第一次迭代指定为 0,之后使用上一次迭代返回的游标;pattern 指定匹配规则;count 指定返回数量

仅当 field 映射的 value 值为数字时才能使用以下命令,否则报错:

命令说明
hincrby / hincrbyfloat key field incrementkey 的 field 的 value 数值 + increment

2.4 Set

Set 是 String 类型的无序集合,集合成员不能重复。底层使用 hashtable,所以添加、删除、查找的复杂度都是 O(1)。

命令说明
sadd key member [member …]往 key 的值集合中添加成员 member,返回添加成功的数量
spop key [count]从 key 的值集合中随机弹出 count 个成员,并返回弹出的成员
smembers key返回 key 的值集合中的所有成员
sismember key member判断成员 member 是否在 key 的值集合中,返回 1 表示存在,返回 0 表示不存在
srandmember key [count]随机返回 key 的值集合中 count 个成员
scard key返回 key 的值集合成员总数
srem key member [member …]从 key 的值集合中删除多个成员 member,返回删除成功的数量
smove source destination member将 source 的值集合中的某个 member 移到 destination 的值集合中,返回 1 表示移动成功
sdiff key [key …]返回属于第一个 key 但不属于后面其它 key 的成员集合
sdiffstore destination key [key …]将属于第一个 key 但不属于后面其它 key 的成员都保存在 destination 的值集合中
sunion key [key …]返回给定 key 的成员并集
sunionstore destination key [key …]将所有 key 的值集合的成员并集保存在 destination 的值集合中
sinter key [key …]返回给定 key 的成员交集
sinterstore destination key [key …]将所有 key 的值集合的成员交集保存在 destination 的值集合中
sintercard numkeys key [key …]返回 numkeys 个 key 之间的交集的成员个数

2.5 Zset

Zset 和 Set 的区别,就是在 Set 的基础上加了一个 score 分数值。底层使用跳表 skiplist 存储。

跳表:单链表每两个节点提取一个节点到上一级索引层,插入节点时新节点与索引层节点逐一比较插入,使得时间复杂度从 O(n) 降到 O(logn),用空间换时间。

跳表的时间复杂度为 O(logN),空间复杂度为 O(N)。

命令说明
zadd key score member [score member]往 key 的值有序集合中添加带评分的成员,返回添加成功的数量
zrem key member [member …]在 key 的值有序集合中删除元素 member ,返回删除成功的数量
zmpop numkeys key [key …] <MINMAX> [COUNT count]
zscore key member返回成员 member 的分数 score
zcard key返回 key 的值有序集合中的成员数量
zincrby key increment memberkey 的值有序集合中成员 member 的评分 + increment,返回新的评分
zcount key min max返回 key 的值有序集合中指定评分范围 [min, max] 内的成员个数
zrank key member从小到大返回 key 的值有序集合中成员 member 的评分排名
zrevrank key member从大到小返回 key 的值有序集合中成员 member 的评分排名
  1. 加上 withscores 选项可以使成员和它的评分一起返回
  2. 如果不想包含边界值 min 和 max,在其左边加上 (
正序说明
zrange key start stop [withscore]从小到大返回 key 的 value 有序集合中下标从 start 到 stop 的成员
zrangebyscore key min max [withscores] [LIMIT offset count]从小到大返回 key 的值有序集合中评分在 min 到 max 区间内的成员(包括 min 和 max)
倒序说明
zrevrange key start stop [WITHSCORES]从大到小返回 key 的值有序集合中下标从 start 到 stop 的成员
zrevrangebyscore key max min [withscores] [LIMIT offset count]从大到小返回 key 的值有序集合中评分在 max 到 min 区间内的成员(包括 min 和 max)

2.6 Bitmap

位图 (bitmap) 是由 0 和 1 表示的二进制位的 bit 数组。

位图底层是 String 类型,本质是数组,数组每一个元素都是一个二进制位,每个二进制位都对应一个偏移量(索引)。Bitmap 使用 512M 内存就可以存储最大位数是 2^32 位字节信息。

Bitmap 的偏移量是从 0 开始:

命令说明
setbit key offset value将下标 offset 的值设为 value (value 只能是 0 或 1),返回该下标原值
getbit key offset返回下标 offset 的值
bitcount key [start end]返回 key 的 value 位图 start 到 end 下标范围内 1 的个数
bitpos key bit [start end]返回 key 的位图中第一个值为 bit 的位置
strlen key获取字节数(8 位一组为 1 个字节)
bitop and destkey key [key …]对多个 key 的位图求逻辑并,并将结果保存到 destkey
bitop or destkey key [key …]对多个 key 的位图求逻辑或,并将结果保存到 destkey
bitop xor destkey key [key …]对多个 key 的位图求逻辑异或,并将结果保存到 destkey
bitop not destkey key对 key 的位图求逻辑非,并将结果保存到 destkey

2.7 HyperLogLog

HyperLogLog 用于基数统计,即对集合去重后剩余元素数量的计算。HyperLogLog 适用于目标为巨量计数,对存储数据内容并不太关心的场景。HyperLogLog 只需要花费 12KB 内存,就能记录 2 的 64 次方个不同元素的基数。HyperLogLog 误差仅仅只是 0.81% 左右。

命令说明
pfadd key element [element …]添加元素 element 到 key 的 HyperLogLog 中,添加成功返回 1
pfcount key [key …]返回给定 key 的 HyperLogLog 中的基数估计值
pfmerge destkey sourcekey [sourcekey …]将多个 sourcekey 的 HyperLogLog 合并到 destkey 的 HyperLogLog 成功返回 OK

2.8 GEO

GEO 的底层根据 GeoHash 算法,将地理位置坐标转换为 52bit 整形数字,存储在 Zset 中。所以也可以用 Zset 的一些命令。

Redis 的有效经度是从 -180 到 180,有效纬度是从 -85.05112878 到 85.05112878。

命令说明
geoadd key longitude latitude member [longitude latitude member …]将一个或多个经度为 longitude 、纬度为 latitude 的地理成员 member 添加到指定的 key 中,返回添加成功的数量
zrem key member [member …]删除 key 的多个地理成员,返回删除成功的数量
geopos key member [member …]返回 key 的所有 member 的经纬度,不存在的返回 nil
geodist key member1 member2 [mkm
georadius key longitude latitude radius [mkm
georadiusbymember key member radius [mkm
geohash key member [member …]获取一个或多个位置元素的 geohash 值

geosearch 使用语法如下:

geosearch key [frommember member|fromlonlat longitude latitude]
[byradius radius unit|bybox width height unit]
[asc|desc] [Count count]
[withcoord] [withdist]

frommember:从已有的 member 中读取经纬度

fromlonlat:从用户参数传递经纬度

byradius:根据给定半径按照圆形搜索,效果同 georadius

bybox:根据给定 width 和 height 按照矩形搜索

withcoord:返回匹配的经纬度

withdist :返回距离

2.9 Stream(阻塞队列)

id 表示消息 id,一般使用 * (由 Redis 生成),Redis 要求最小 id 为 0-1,并且后续 id 不能小于前一个 id:

生产者命令说明
xadd key id field value [field value …]向 key 队列添加消息[field value],添加成功返回消息 id
xdel key id [id …]根据消息 id 删除 key 队列的流中的对应消息
xtrim key maxlen [count]截取使得 key 队列的流的最大长度为 count(先截取 id 小的)
xtrim key minid截取使得 key 队列的流的最小 id 不小于 minid
  • 表示最小消息 id,+ 表示最大消息 id:
生产者命令说明
xlen key向 key 队列的流中添加消息[field value],添加成功返回消息 id
xrange key start end [COUNT count]正向获取 key 队列的流中的 count 条消息
xrange key - +正向返回 key 队列的流中的每一条消息
xrevrange key end start [COUNT count]逆向返回 key 队列的流中的 count 条消息
xrevrange key + -逆向返回 key 队列的流中的每一条消息

milliseconds 可选,表示阻塞毫秒数,没有设置就是非阻塞模式:

生产者命令说明
xread [COUNT count] streams key [key …] id [id …]非阻塞读取指定 id 的 count 条消息
xread [COUNT count] streams key [key …] 0-0非阻塞读取从最小 id 开始的 count 条消息
xread streams key [key …] 0-0非阻塞读取所有消息
xread [COUNT count] [BLOCK milliseconds] streams key [key …] id [id …]阻塞读取指定 id 的 count 条消息
xread count 1 block 0 streams key $阻塞读取 key 队列的流中的最新消息

id 为 0 表示从头开始消费,id 为 $ 表示消费最新的消息,> 表示还没有发送给消费组的消息,发送后更新消费组的最后 id:

消费者命令说明
xgroup create key groupname 0-0创建消费者组 groupname 从 key 的流头部开始消费
xgroup create key groupname $创建消费者组 groupname 从 key 的流尾部开始消费
xreadgroup group group consumer streams key >消费者组 group 的消费者 consumer 读取 key 的流中的所有未发送消息
xreadgroup group group consumer [COUNT count] streams key >消费者组 group 的消费者 consumer 读取 key 的流中 count 条消息。实现消息读取的负载均衡

同一消息一旦被某消费组的一个消费者读取,就不能被该消费组的其他消费者读取。不同消费组的消费者则可以消费同一条消息。

消费者命令说明
xgroup create key groupname 0-0创建消费者组 groupname 从 key 的流头部开始消费
xgroup create key groupname $创建消费者组 groupname 从 key 的流尾部开始消费
xreadgroup group group consumer streams key >消费者组 group 的消费者 consumer 读取 key 的流中的所有未发送消息
xpending key group查询消费者组 group 内所有消费者已读取但尚未确认的消息
xpending key group - + [COUNT count] comsumer查询消费者组 group 内消费者 comsumer 已读取但尚未确认的 count 条消息
xack key group id [id …]消费者组 group 内指定 id 的消息标记为已确认

3 Redis 持久化

3.1 RDB

3.1.1 RDB 含义

RDB 是 Redis 在指定的时间间隔,将内存数据全部保存到磁盘的 dump.rdb 文件中。即使 Redis 服务器故障宕机,磁盘中的快照文件 dump.rdb 也不会丢失,数据的可靠性也就得到了保证。

优点如下:

  1. 适合大规模的数据恢复
  2. 按照业务定时备份
  3. 对数据完整性和一致性要求不高
  4. RDB 文件在内存中的加载速度比 AOF 快得多

缺点如下:

  1. Redis 服务器宕机前最后一次持久化的数据可能会丢失
  2. 如果备份数据很大可能 fork 子进程会很耗时(子进程需要先复制一份主进程的数据)
3.1.2 RDB 备份方法
  1. 自动备份

redis.conf 配置如下:

# 执行 RDB 备份的条件:
# 每隔 3600 秒内有 1 个修改
# 或每隔 300 秒内有 100 个修改
# 或每隔 60 秒内有 10000 次修改 
#
save 3600 1 
save 300 100 
save 60 10000
  1. 手动备份

    1. save:在主线程中执行会阻塞 Redis 服务器,直到持久化工作完成才能处理其他命令,生产环境禁止使用
    2. bgsave(推荐):fork 一个子进程执行持久化过程,在后台异步进行快照操作,不阻塞 Redis 服务器。lastsave 命令可以获取最后一次 bgsave 成功的时间
# 执行一次 bgsave
redis-cli -c bgsave 

# 获取最后一次 bgsave 成功的时间
redis-cli -c lastsave 
  1. 异常修复

RDB 文件 dump.rdb 有可能备份异常,使用 redis-check-rdb 命令修复 RDB 文件:

redis-check-rdb dump.rdb
3.1.3 RDB 底层原理

Redis 服务器不仅需要服务线上请求,同时还要备份内存快照。在备份快照过程中 Redis 必须进行文件 IO 读写,而 IO 操作会严重影响服务器性能。所以,Redis 利用操作系统写时复制(Copy On Write)机制实现 RDB。

如果主进程读取内存数据,那么和 bgsave 的子进程并不冲突。如果主进程要修改内存数据(比如图中数据 C),那么操作系统内核会将要被修改的数据复制一份(复制的是修改之前的数据),未被修改的内存数据仍被父子进程共享,被主进程修改的内存空间归属于主进程,被复制出来的数据归属于子进程。如此一来,主进程可以在子进程 bgsave 时肆无忌惮地接受新数据的写入,子进程也能够对某时刻的数据做快照。

当然,如果短时间有大量写请求进来,内存复制的压力也自然不小。

3.2 AOF(Append Only File)

3.2.1 AOF 含义

AOF 以日志的形式来记录每个写操作,将 Redis 执行过的所有写指令记录下来,只许追加文件但不可以改写文件,Redis 启动时都会读取该文件将写指令从前到后执行一次重新构建数据。

优点如下:

  1. 更好的保护数据不丢失、性能高、可做紧急恢复
  2. 当 AOF 文件太大时,Redis 能在后台自动重写 AOF

缺点如下:

  1. 相同数据集的数据而言 AOF 文件要远大于 RDB 文件
  2. AOF 恢复速度慢于 RDB
3.2.2 AOF 工作流程

三种写回策略:

  1. always:同步写回,每个写命令执行完立刻同步地将日志写回磁盘
  2. everysec:每秒写回(默认),每个写命令执行完,先把日志写到 AOF 缓冲区,每隔 1s 把缓存区数据写入磁盘
  3. no:操作系统控制写回,只是将日志先写到 AOF 缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
3.2.3 AOF 数据恢复
  1. 正常恢复

默认情况下,Redis 没有开启 AOF,开启 AOF 功能需要在 redis.conf 中设置:

appendonly yes

在 Redis 的 MP-AOF(Multi Part AOF)中,将单个 AOF 文件分为多个 AOF 文件。有以下三种类型:

  • BASE:表示基础 AOF,它一般由子进程通过重写产生,该文件最多只有一个
  • INCR:表示增量 AOF,它一般会在 AOF 重写开始执行时被创建,该文件可能存在多个
  • HISTORY:表示历史 AOF,每次 AOF 重写成功完成时,本次重写之前对应的 BASE 和 INCR AOF 都将变为 HISTORY,HISTORY 类型的 AOF 会被 Redis 自动删除
  1. 异常恢复

如果 AOF 文件写了错误的指令,使用 redis-check-aof --fix 命令进行修复:

redis-check-aof --fix appendonly.aof.1.incr.aof
3.2.4 AOF 重写机制(⭐)

AOF 文件随着写命令的增加不断变大,当 AOF 文件大小超过设定峰值,Redis 需要启动子进程进行 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。

  1. 自动重写

在 redis.conf 配置文件中默认为:

# 当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时,自动开启重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
  1. 手动重写
bgrewriteaof

下图展示的是在 MP-AOF 中执行一次 AOF 重写的大致流程。在开始时我们依然会 fork 一个子进程进行重写操作,在主进程中,我们会同时打开一个新的 INCR 类型的 AOF 文件,在子进程重写操作期间,所有的数据变化都会被写入到这个新打开的 INCR AOF 中。子进程的重写操作完全是独立的,重写期间不会与主进程进行任何的数据和控制交互,最终重写操作会产生一个 BASE AOF。新生成的 BASE AOF 和新打开的 INCR AOF 就代表了当前时刻 Redis 的全部数据。

重写结束时,主进程会负责更新 manifest 文件,将新生成的 BASE AOF 和 INCR AOF 信息加入进去,并将之前的 BASE AOF 和 INCR AOF 标记为 HISTORY(这些 HISTORY AOF 会被 Redis 异步删除)。一旦 manifest 文件更新完毕,就标志整个重写流程结束。

3.3 RDB - AOF 混合持久化

在 redis.conf 配置文件中设置开启 RDB 和 AOF 混合持久化:

aof-use-rdb-preamble yes

开启混合持久化后,AOF 重写时会把 Redis 的持久化数据以 RDB 格式(以 REDIS 开头)写入到 AOF 文件开头,之后的增量数据则以 AOF 的格式(以 * 开头)追加在后面。即 RDB 做全量持久化,AOF 做增量持久化。

混合持久化的加载流程如下:

  1. 判断是否开启 AOF 持久化,开启继续执行后续流程,未开启执行加载 RDB 文件的流程
  2. 判断 appendonly.aof 文件是否存在,文件存在则执行后续流程
  3. 判断 AOF 文件开头是 RDB 的格式,先加载 RDB 内容再加载剩余的 AOF 内容
  4. 判断 AOF 文件开头不是 RDB 的格式,直接以 AOF 格式加载整个文件

4 Redis 集群

单个主节点难以承担过大的数据量,因此需要水平扩展。每个节点只负责存储整个数据集的一部分,这就是 Redis 的集群,其作用是提供在多个 Redis 节点间共享数据的程序集。

Redis 集群支持多个主节点,每个主节点又可以挂载多个副本(从节点)。客户端要和 Redis 集群连接,只需连接集群中的任意一个可用节点即可

4.1 集群分布式存储(⭐)

4.1.1 哈希槽

为了解决均匀分配的问题,在数据和节点之间又加入了一层哈希槽(slot),用于管理客户端数据和 Redis 节点之间的关系。Redis 节点上放的是槽,槽里放的是数据。

哈希槽的优点是方便扩缩容。比如现有 3 个主节点 A、B、C ,每个主节点上有 5461 个槽,再添加 1 个主节点 D 将 A、B 、C 上的槽匀给 D,使得每个主节点有 4096 个槽。如果要移除主节点 A,则将 A 的槽移动到其余主节点上,哈希槽移动时不会停止服务,保证集群服务的高可用性。

4.1.2 哈希槽数 16384

Redis 集群共有 16384 个槽,平均分布在集群内的每个主节点上。副本只能读取槽内的数据。

CRC16 算法产生的哈希值有 16 bit,即槽位最大可以有 2^16 = 65536 个值。如果槽位为 65536,发送心跳信息的消息头达 8k,发送的心跳包过于庞大,而槽位为 16384 发送心跳信息的消息头为 2k。

Redis 作者不建议集群节点数量超过 1000 个。对于节点数在 1000 以内的集群,16384 个槽位就够用了。

4.1.3 哈希槽的哈希算法

每个 key 通过 CRC16 算法校验,然后对总数 16384 取模,即 CRC16(key) mod 16384。

4.2 集群搭建

4.2.1 准备三台虚拟机

在三台虚拟机上新建目录,用于放配置文件:

mkdir -p /redis/cluster
4.2.2 新建 6 个 Redis 节点

第一台虚拟机: 主机端口 6382 + 从机端口 6383

端口 6382 为主机,为其创建配置文件:

vim /redis/cluster/Cluster6382.conf

Cluster6382.conf 文件如下:

bind 0.0.0.0
daemonize yes
protected-mode no
port 6382
logfile "/redis/cluster/cluster6382.log"
pidfile /redis/cluster6382.pid
dir /redis/cluster
dbfilename dump6382.rdb
appendonly yes
appendfilename "appendonly6382.aof"
requirepass 123
masterauth 123
 
cluster-enabled yes
cluster-config-file nodes-6382.conf
cluster-node-timeout 5000

端口 6383 为从机,为其创建配置文件:

vim /redis/cluster/Cluster6383.conf

Cluster6383.conf 文件如下:

bind 0.0.0.0
daemonize yes
protected-mode no
port 6383
logfile "/redis/cluster/cluster6383.log"
pidfile /redis/cluster6383.pid
dir /redis/cluster
dbfilename dump6383.rdb
appendonly yes
appendfilename "appendonly6383.aof"
requirepass 123
masterauth 123
 
cluster-enabled yes
cluster-config-file nodes-6383.conf
cluster-node-timeout 5000

其他两台同理操作。

4.2.3 启动 6 台 Redis

在三台虚拟机上分别启动 Redis:

redis-server /redis/cluster/Cluster6382.conf 
redis-server /redis/cluster/Cluster6383.conf 
ps -ef | grep redis

redis-server /redis/cluster/Cluster6384.conf 
redis-server /redis/cluster/Cluster6385.conf
ps -ef | grep redis

redis-server /redis/cluster/Cluster636.conf 
redis-server /redis/cluster/Cluster6387.conf
ps -ef | grep redis
4.2.4 通过 redis-cli 命令构建集群关系

–cluster create 表示搭建集群,–cluster-replicas 1 表示每个 master 创建一个 slave:

redis-cli -a 123 --cluster create --cluster-replicas 1 192.168.239.128:6382 192.168.239.128:6383 192.168.239.129:6384 192.168.239.129:6385 192.168.239.130:6386 192.168.239.130:6387


4.2.5 通过 cluster nodes 命令检验集群状态

登录 Redis 现在必须带上端口号,且必须带上 -c 参数,表示按照集群启动

redis-cli -a 123 -p 6382 -c

# 输入 cluster nodes 查看集群节点的连接状态,它们的标志,属性和分配的槽
127.0.0.1:6382> cluster nodes

# 输入 set k1 v1,我们发现虽然是在 6382 端口写入的 key,但是重定向到集群其它端口(6386 端口)的 12706 槽位
127.0.0.1:6382> set k1 v1

4.2.6 扩缩容命令

集群新增节点:

redis-cli -a <password> --cluster add-node <新节点ip>:<新节点port> <原节点ip>:<原节点port>
redis-cli -a <password> --cluster check <原节点ip>:<原节点port>

# reshard 重新分配槽号
redis-cli -a <password> --cluster reshard <原节点ip>:<原节点port>
redis-cli -a <password> --cluster check <原节点ip>:<原节点port>

集群移除节点:

redis-cli -a <password> --cluster del-node <要删除节点ip>:<要删除节点port> 

# reshard 重新分配槽号
redis-cli -a <password> --cluster reshard <原节点ip>:<原节点port>

5 Redis 单线程

Redis 单线程指的是网络 IO 和命令执行是由一个主线程串行处理,包括「接收请求 -> 解析 -> IO 读写 -> 返回数据」过程,这也是我们常说 Redis 是单线程的原因。

Redis 的命令执行是纯内存操作,只要采用合适的数据结构和算法完全可以支撑成千上万高并发。所以,Redis 的性能瓶颈在于 IO 层面。

从 Redis6 开始,新增多线程功能提高 IO 的读写性能,将原来主线程的「接受请求 -> 解析 -> IO 读写」任务交给一组独立的 IO 线程去做,采用 IO 多路复用技术使单线程可以同时处理多个请求。

Redis 单线程一次性可以从内核 TCP 队列接收最多 1000 个新连接(IO 多路复用),然后将这些新连接对应的文件描述符注册到内核进行 IO 事件的监听。

所以,Redis 使用单线程但性能高的原因总结如下:

  1. 大部分操作都在内存中完成,运算性能高
  2. 使用 IO 多路复用和非阻塞 I O 来监听多个 socket 连接客户端,实现单个线程处理多个请求,减少线程切换开销和 IO 阻塞(最主要原因)

6 Redis BigKey 问题

  • string 类型的 value >= 10KB 就是 BigKey
  • list、hash、set 和 zset 类型的 value,个数超过 5000 就是 BigKey

6.1 配置禁用危险命令

keys * / flushall / flushdb 命令严禁在线上使用,因为这些命令会造成阻塞,会导致 Redis 其他的读写都被延后甚至是超时报错,可能会引起 缓存雪崩 甚至数据库宕机。

在实际开发中,我们在配置文件中添加以下配置,可以禁用这几个危险命令:

rename-command keys ""
rename-command flushdb ""
rename-command flushall ""

6.2 scan 命令

scan 命令是一个基于游标的迭代器,每次被调用之后,都会返回一个包含两个元素的数组,第一个元素是用于进行下一次迭代的新游标(返回 0 表示迭代结束),第二个元素则是一个包含了所有被迭代元素的数组。

# curson 游标
# pattern 匹配规则(无规则写 *)
# count 返回数据数量
scan curson match [pattern] [count]

6.3 删除 BigKey 的方法

  1. String 类型一般用 del / unlink
  2. List 类型使用 ltrim 渐进删除
  3. Hash 类型使用 hscan 每次获取部分元素,再使用 hdel 删除每个元素
  4. Set 类型使用 sscan 每次获取部分元素,再使用 srem 命令删除每个元素
  5. Zset 类型使用 zscan 每次获取部分元素,再使用 zremrangebyrank 命令删除每个元素

7 数据库缓存一致性

数据库缓存一致性要求:如果 Redis 中有数据,需要和数据库中的值相同;如果 Redis中无数据,数据库中的值是最新值,且准备回写 Redis。

数据一致性是一个复杂的课题,通常是多种策略同时使用,例如:延时双删、Redis 过期淘汰、通过路由策略串行处理同类型数据、分布式锁等。

7.1 缓存双写一致性

  1. 同步直写策略:写数据库后也同步写 Redis 缓存,缓存中的数据和数据中的一致

  2. 异步缓写策略:在实际业务中,MySQL 数据变动了,但是在业务上容许出现一定时间后才作用于 Redis,比如仓库、物流系统等。异常情况出现了,需要借助 kafka 或者 RabbitMQ 等消息中间件,实现重写

  3. 双检加锁策略

    1. 加锁前先查 Redis,有直接返回结果
    2. 加锁避免击穿 MySQL,加锁后再查 Redis,有直接返回结果,没有再去查询 MySQL
    3. MySQL数据回写 Redis,实现数据一致性

7.2 数据库缓存一致性更新策略

一般业务会将 Mysql 作为底单数据库,有最终解释权。所以一般先操作数据库,再操作缓存:

策略异常问题解决方案
先删除缓存,再更新数据库删除缓存后,若数据库更新失败或返回不及时,导致其他线程直接去数据库读到旧值,并将该旧值回写到缓存中延时双删
先更新数据库,再删除缓存(推荐)数据库更新后,若缓存删除失败或删除不及时,导致其他线程读到缓存中的旧值binlog + 消息队列

7.3 延时双删

延时双删实现数据库和缓存数据最终一致性,不适合要求低延迟和数据强一致的场景。

  1. 删除 Redis 缓存数据
  2. 更新数据库数据
  3. 当前逻辑延时执行,具体需要自行评估项目的读数据业务耗时,写数据休眠时间在读数据业务耗时基础上加百毫秒即可
  4. 再次异步删除 Redis 缓存数据

7.4 binlog + 消息队列

8 布隆过滤器

面试题:现有 10W 个电话号码,如何快速判断这些电话号码是否存在 / 白名单校验。

8.1 布隆过滤器概念

布隆过滤器由一个初值都为 0 的 bit 数组和多个 哈希函数 构成,用来快速判断集合中是否存在某个元素。 布隆过滤器不保存数据信息,只是在内存中做一个是否存在的标记。

8.2 布隆过滤器原理

  1. 添加 key:使用多个 hash 函数对 key 进行 hash 运算得到一个整数索引值,然后对 bitmap 数组长度取模运算得到一个位置。每个 hash 函数都会得到一个不同的位置,将这几个位置都置 1 就完成了添加操作
  2. 查询 key:只要有其中一位是零就表示这个 key 不存在,但如果都是 1,则不一定存在对应的 key(有,可能有;无,一定无)

8.3 布隆过滤器优缺点

  1. 优点:高效地插入和查询,内存占用 bit 空间少
  2. 缺点:不能删除元素,删除元素可能把同一个位置的其他元素删除,导致误判率增加;存在误判

9 缓存穿透 / 缓存击穿 / 缓存雪崩

  1. 缓存穿透:每次对 key 的请求在缓存中获取不到,且数据库中不存在 key 对应的数据,导致缓存被穿透,数据库压力暴增。 解决办法:

    1. 空值缓存或缺省值:如果有大量相同的不存在的 key 到来,对返回的空结果进行缓存或者设置为缺省值 defaultNull,这样在之后该 key 到来时能从缓存中读到值,减小对数据库的压力
    2. 使用布隆过滤器:设置一个白名单,对白名单上不存在的 key 进行拦截
  2. 缓存击穿:某个 key 过期时间内来了大量请求,瞬间把数据库压垮。 解决办法:

    1. 缓存预热:提前将热点 key 放在缓存中,并增加其过期时间
    2. 使用分布式锁
  3. 缓存雪崩:缓存中热点 key 短时间内失效过期,在过期时间内大量请求过来瞬间让数据库崩溃。 解决办法:

    1. 设置过期时间基础上加上一个随机值,使每个 key 过期时间错开
    2. 构建多级缓存架构
    3. 构建缓存集群实现高可用

10 基于单实例的分布式锁

10.1 单实例加锁

不可以使用 setnx 命令加锁,因为该命令将加锁和过期命令分开设置,有可能出现死锁。使用以下命令:

# 加锁和过期命令放在一起,避免死锁
set key value ex [seconds] nx 
set key value px [milliseconds] nx

优化 1:我们可以使用 hincrby 命令替代 set、hest 命令保证锁的可重入性,field 设为锁的 UUID + 线程 ID,value 设置锁的重入次数。下次重入可以使用 hincrby 添加 / 减小重入次数。

hincrby [key] [field] [value] 

上述命令加锁和解锁命令不是原子性的,会出现解错锁的问题。优化 2:需要引入 lua 脚本解决。

10.2 lua 脚本

Redis 中使用 lua 脚本的格式,numskeys 表示传参的数量,后面跟上传参的 key 和 arg:

eval "luascript" [numkeys] [key[key ...]] [arg[arg ...]]
  1. 保证加锁 lock 的可重入性:
eval "
if redis.call('exist', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
    redis.call('hincrby', KEYS[1], ARGV[1], 1)              -- hincrby 加锁  
    redis.call('expire', KEYS[1], ARGV[2])                  -- 设置过期时间          
    return 1
else
    return 0  
end"  
3 RedisLock [uuid]:[threadid] 50 
-- key 是分布式锁名字 RedisLock
-- arg1 是锁的 id,arg2 是锁的过期时间(s)
  1. 保证解锁 unlock 的原子性:
eval "
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
    return nil                                                    -- 无锁
elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then
    return redis.call('del', KEYS[1])                             -- 可重入次数为 0 后解锁
else
    return 0
end"
3 RedisLock [uuid]:[threadid]    

锁可重入次数为 3:

当可重入次数为 0 时,执行解锁,返回 1 表示解锁成功,再执行解锁脚本时已经无锁,返回 nil:

10.3 lua 脚本整合 Java 微服务

新建 DistributedLockFactory 工厂提供各种分布式锁的构造:

@Component
public class DistributedLockFactory {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuid;
    public DistributedLockFactory() {this.uuid = UUID.randomUUID().toString();}

    public Lock getDistributedLock(String lockType) {
        if (lockType == null) return null;
        if (lockType.equalsIgnoreCase("redis")) {
            this.lockName = "RedisLock";
            return new MyRedisLock(stringRedisTemplate, lockName, uuid);
        } else if (lockType.equalsIgnoreCase("zookeeper")) {
            this.lockName = "ZookeeperLock";
            // TODO zookeeper 版本的分布式锁
            return null;
        } else if (lockType.equalsIgnoreCase("MySQL")) {
            this.lockName = "MySQLLock";
            // TODO MySQL 版本的分布式锁
            return null;
        }
        return null;
    }
}

新建 MyRedisLock 类并实现 Lock 接口:

public class MyRedisLock implements Lock {
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long expireTime;

    public MyRedisLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuid + ":" + Thread.currentThread().getId();
        this.expireTime = 20L;
    }

    @Override
    public void lock() {tryLock();}

    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        return false;
    }

    // 加锁逻辑
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L) {
            String script = "if redis.call('exist', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end";
            // 加锁失败自旋
            while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
                try {
                    TimeUnit.MILLISECONDS.sleep(60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return true;
        }
        return false;
    }

    // 解锁逻辑
    @Override
    public void unlock() {
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end";
        
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if (flag == null) {
            throw new RuntimeException("锁不存在");
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

调用 MyRedisLock 对减库存业务进行加解锁:

@Service
public class RedisLockService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Autowired
    private DistributedLockFactory distributedLockFactory;

    public String sale() {
        String message = "";
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try {
            // 查库存
            String res = stringRedisTemplate.opsForValue().get("iphone");
            // 判断库存是否足够
            Integer phoneNum = res == null ? 0 : Integer.parseInt(res);   
            
            if (phoneNum > 0) {
                stringRedisTemplate.opsForValue().set("iphone", String.valueOf(--phoneNum)); 
                message = "成功卖出一台苹果手机,库存剩余:" + phoneNum;
                System.out.println(message);
            } else {
                message = "手机没货了,555";
            }
        } finally {
            redisLock.unlock();
        }
        return message;
    }
}

11 Redisson

11.1 Redisson 概述

Redisson 是 Java 基于 Redis 实现的分布式工具,采用的是 RedLock 算法。Redisson 相比于手写分布式锁的优势在于基于多个实例建立分布式锁,防止单个节点宕机后一锁多用的危险场景。

Redisson 底层源码实现(看门狗的锁超时时间默认为 30s,每隔 30 * 1/3 = 10s 检查是否还持有锁):

11.2 Redisson 算法原理

在 Redis 分布式环境中,我们假设有 N 个 master 节点(N 一般为 5)。这些节点完全互相独立,不存在主从关系或者集群关系。我们确保在 N 个实例上使用上一节的方法加解锁。我们需要在 5 台机器上运行 5 个实例,保证它们不会同时宕机。为了获得锁(加锁),客户端应该执行以下操作:

  1. 获取当前 Unix 时间(ms)
  2. 客户端使用相同的 key 和随机值锁 id(例如 UUID)从所有 N 个实例中顺序获取锁。客户端设置了获取锁的超时时间,如果获取锁超时,则客户端尽快去下一个 Redis 实例中请求获取锁
  3. 客户端通过当前 Unix 时间减去获取到锁的时间来计算获取锁的使用时间。当且仅当从一半以上(N / 2 + 1)的 Redis 实例中都取到锁,并且锁的使用时间小于锁的过期时间,锁就算获取成功
  4. 如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功,防止某些节点获取到锁,但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)

11.3 SpringBoot 配置多机 Redisson

Docker 启动 3 台独立的 master:

docker run -p 6381:6379 --name redis-master1 -d redis
docker run -p 6382:6379 --name redis-master2 -d redis
docker run -p 6383:6379 --name redis-master3 -d redis

在模块的 pom.xml 添加依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.19.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.4</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.11</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-configuration-processor</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

</project>

修改 application.properties 配置文件

server.port=9090
spring.application.name=redlock

spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single

spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10

spring.redis.single.address1=192.168.239.128:6381
spring.redis.single.address2=192.168.239.128:6382
spring.redis.single.address3=192.168.239.128:6383

添加 RedisSingleProperties 配置类:

@Data
public class RedisSingleProperties {
    private String address1;
    private String address2;
    private String address3;
}

添加 RedisPoolProperties 配置类:

@Data
public class RedisPoolProperties {
    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;
    private int connTimeout = 10000;
    private int soTimeout;
    private int size;
}

添加 RedisProperties 配置类:

@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {
    private int database;
    private int timeout = 3000;
    private String password;
    private String mode;
    private RedisPoolProperties pool;
    private RedisSingleProperties single;
}

启动项目,在浏览器输入 http://localhost:9090/multilock 。在 30s 内打开虚拟机使用以下命令进入 Redis:

docker exec -it redis-master1 redis-cli

不断输入 TTL SY_REDLOCK,可以发现过期时间减少 10s 左右时会自动续到 30s,直到 -2 表示已解锁:


  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值