2025-08-02-Sat-T-Redis

基础篇

1. 常见数据结构和命令

1.1 五种常见数据结构

1
2
3
4
5
String
Hash
Set
List
SortedSet

1.2 五种扩展数据结构

(1) Redis 位图(Bitmap)

位图本质是数组,它是基于String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们称之为一个索引)。
Bitmap支持的最大位数是2^32位,它可以极大的节约存储空间,使用512M内存就可以存储多达42.9亿位信息(2^32=4294967296)

1
2
3
4
5
6
7
SETBIT key offset value
GET key
GETBIT key offset
STRLEN key # 求当前bimap的key对应的值占了多少字节。8 个bit位为一个字节,如果只设置了setbit k1 32 0. 那么k1占5个字节. (使用了就会增加)
bitcount key # 求value中1的个数
bitop and/or/xor/not distkey key1 key2 key3 key4 # 对key1 key2 key3..进行与或非操作。 XOR异或操作

用户登录状态

数据是否被访问过

签到统计、在线用户统计

(2)基数统计(HyperLogLog)

统计某个网站的UV(用户访问量)

用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算。

去重过程中可能会有一点误差 0.81%

1
2
3
PFADD key element1 element2..  # 将元素添加到集合key
PFCOUNT key1 key2... # 统计key1, key2等集合中不重复元素的个数
PFMERGE destkey sourcekey1 sourcekey2...# 将sourcekey1 和 sourcekey2集合合并为destkey

(3)地理空间(GEO)

1
2
3
4
5
6
GEOADD key longitude latitude m1 lon2 lat2 m2 ... # 创建一个key,并设置经度、维度坐标值
GEOPOS key member [member...] # 返回成员坐标位置
GEODIST key m1 m2 # 返回两个坐标的距离
GEORADIUS key longitude latitude radius # 返回以某个坐标下半径为radius的成员
GEORADIUSBYMEMBER key m1 radius # 返回以成员m1为圆心,radius为半径的圆内部的成员
GEOHASH key [memeber ...] # 获取key的哈希值

(4)Redis流(Stream)

Redis实现消息队列的方案:

  • List实现: 点对点模式
  • Pub/Sub:发布订阅模式, 无法持久化
  • Stream:消息中间件 + 阻塞队列
1
XADD key * field1 value1 field2 value2...

(5)Redis位域(bitfield)

将一个Redis字符串看作是一个由二进制位组成的数组并能对变长位宽和任意没有字节对齐的指定整型位域进行寻址和修改

比如修改一个’a’字符为’c’, 那么只需要将’a’的某几个bit位修改一下即可。

1.3 通用命令

1
2
3
4
5
keys `pattern` # 查询有哪些key
del `key` # 删除key
exists `key` # 判断是否存在key
expire `key` `seconds` # 设置有效期
ttl `key` # 查询有效期

1.4 不同数据结构的操作命令

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
# String
set `key` `value`
setnx `key` `value` # 不存在才set
setex `key` `value` `ttl`
incrby `key` `increment` # 自增

# Hash 哈希表
hset `key` `field` `value` `field` `value`
hget `key` `field`
hsetnx `key` `value` # 不存在才set

# List 双向链表
LPUSH `key` `element1` `element2` ... # 从左侧插入一个或多个元素
LPOP `key` # 移除左侧第一个元素,没有返回null
RPUSH `key` `element1` `element2` ...
RPOP `key`
LRANGE `key` `start` `end` # 返回start到end之间的元素

# Set 无序集合
SADD `key` `member1` `member2` # 添加
SREM `key` `member1` `member2` # 删除
SCARD `key` # 返回set中元素的个数
SISMEMBER `key` `membern` # 判断是否是key成员
SmemberS `key` # 查询key的所有成员
SINNER `key1` `key2` # 获取交集
SDIFF `key1` `key2` # 求差集, key1有,key2没有
SUNION `key1` `key2` # 求并集

# Zset/SortedSet ### 跳表 + Hash表, 有序集合,查询效率高
ZADD `key` `score1` `member1` `score2` `member2` ... # 添加一个或多个元素到sorted set,如果存在更新score
ZREM `key` `member` # 删除
ZSCORE `key` `member` # 获取元素的score
ZRANK `key` `member` # 获取元素排名
ZCARD `key` # 获取元素个数
ZCOUNT `key` `min` `max` # 求指定范围score的元素个数
ZINCRBY `key` `increment` `member` # 对指定member自增
ZRANGE `key` `min` `max` # 获取指定排名范围的元素
ZRANGEBYSCORE `key` `min` `max` # 获取指定score范围内的元素

2. Jedis的应用与优化

3. SpringDataRedis的应用和优化

实战篇

1. 共享session

2. 企业缓存解决方案

2.1 缓存更新/淘汰策略

缓存更新策略的最佳实践方案

  1. 低一致性需求: 使用Redis自带的内存淘汰机制
  2. 高一致性需求: 主动更新,并以超时剔除作为兜底方案
    • 读操作:
      • 缓存命中则直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作:
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

2.2 缓存穿透、缓存雪崩、缓存击穿

维度 缓存击穿 缓存雪崩 缓存穿透
定义 单个热点 key 过期时,大量并发请求瞬间击穿缓存直达数据库 大量缓存 key 同时失效或缓存服务宕机,导致请求洪涌压垮数据库 请求不存在的数据,缓存与数据库均未命中,反复穿透缓存
核心解决方案 互斥锁 1. 随机过期时间:基础 TTL + 随机偏移量
2. 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis)
3. 熔断限流:Hystrix/Sentinel 保护数据库
1. 布隆过滤器:拦截非法请求(误判率可控)
2. 缓存空值NULL 结果短时缓存(如 5 分钟)
3. 参数校验:过滤非法 ID(如 ID≤0)

2.3 全局唯一ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RedisIdWorker {
private static final long BEGIN_TIMESTAMP = 1640995200L;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}

public long nextId(String keyPrefix) {
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;

//生成序列号
DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyyMMdd");
// 自增长
Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
return timestamp << 32 | increment; // 时间戳左移32位,与上inrc key的自增长数
}
}

2.4 缓存同步策略

缓存数据同步的常见方式有三种:

  • 设置有效期: 给缓存设置有效期,到期后自动删除。再次查询时更新
    • 优势: 简单、方便
    • 缺点: 时效性差,缓存过期之前可能不一致
    • 场景: 更新频率较低,时效性要求低的业务
  • 同步双写: 在修改数据库的同时,直接修改缓存
    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据
  • 异步通知: 修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

3. 秒杀中的Redis应用

3.1 分布式锁

3.1.1 防误删 - 唯一标识

key : 锁名称 value : 唯一标识

场景 推荐标识方案 技术要点
通用分布式锁 UUID + 线程 ID 轻量、全局唯一,适用 90% 场景。
需问题追溯的物理部署 IP + PID + 线程 ID 结合运维监控系统(如 Prometheus),快速定位故障节点。
可重入锁(如递归逻辑) JSON 结构化数据 记录重入次数,避免同一线程死锁。
容器化环境(K8s) Pod ID + 容器内线程 ID 通过 Downward API 注入 Pod 名称,替代 IP。

3.1.2 原子操作 - Lua脚本

Redis提供了Lua脚本功能, 在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言。基础语法参考: Lua 教程 | 菜鸟教程

1
EVAL `script` `numkeys` [key [key ...]] [arg [arg ...]] # 数组下标从1开始
1
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 "name" "jack"; # 数组下标从1开始
1
2
3
4
5
6
-- 原子操作: "查询" + "比较" + "删除"
local id = redis.call('get',KEYS[1])
if(id == ARGV[1]) then
return redis.call('del',KEYS[1])
end
return 0
1
2
3
public void unlock(){
stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY),UNIQUE_ID);
}

3.1.3 Redisson

Redisson | Valkey & Redis Java client. Ultimate Real-Time Data Platform

Github : redisson/redisson

锁类型 核心类 实现原理 作用 网络依赖 性能极限 适用场景 关键特性
Redis原生锁 SET key uuid NX PX 通过SETNX命令原子性设置键值(不存在才设置),搭配过期时间和Lua脚本释放锁 基础互斥锁,轻量高效 单Redis节点 10万+ TPS 高并发场景(如秒杀、缓存更新) 需处理锁续期和主从切换风险
可重入锁 RedissonLock 基于Redis Hash结构:Key=锁名,Field=UUID:线程ID,Value=重入次数;Lua脚本原子操作;看门狗自动续期(默认30秒) 同一线程可重复获取锁,避免死锁 单Redis节点或集群 ≤5k TPS 通用互斥场景(如库存扣减、递归调用) ✅ 自动续期 ✅ 非公平模式(默认) ❌ 公平性无保障
公平锁 RedissonFairLock 基于Redis List实现FIFO队列 + ZSet记录超时时间;新请求入队,仅队首线程可获锁;Pub/Sub通知唤醒 按请求顺序分配锁,避免线程饥饿 单Redis节点或集群 ≤3k TPS 顺序敏感业务(如订单支付、任务调度) ✅ 严格公平性 ❌ 性能较低(维护队列开销)
联锁 RedissonMultiLock 组合多个独立锁(RLock实例);原子性操作:所有锁成功才算成功,否则立即回滚 同时锁定多个资源,保证原子性 同Redis集群的多节点 ≤1k TPS 多资源操作(如跨账户转账、分布式事务) ❌ 性能受最慢子锁影响 ✅ 原子性保障 ⚠️ 死锁风险高
红锁 RedissonRedLock Redlock算法:在N个独立Redis节点上加锁,成功数 ≥ N/2+1才算有效;时钟同步优化 高可用容错,防主从切换丢锁 多个独立Redis节点(≥3) ≤500 TPS(5节点) 金融交易、支付系统等高可靠性场景 ✅ 容忍部分节点故障 ❌ 运维复杂 ⚠️ 网络开销大
读写锁 RedissonReadWriteLock 双锁结构:读锁(共享锁) + 写锁(互斥锁);写锁优先阻塞后续读锁 读读并发,读写/写写互斥 单Redis节点或集群 读≤10k TPS 写≤1k TPS 读多写少场景(如缓存、配置中心) ✅ 提升读并发性 ❌ 写锁饥饿风险 ⚠️ 不支持锁降级
信号量 RedissonSemaphore Redis计数器(incrby/decrby) + 订阅发布控制等待线程 限制并发线程数(限流/资源池) 单Redis节点或集群 ≥10k TPS 连接池管理、秒杀系统限流 ✅ 轻量高效 ❌ 无重入支持 ⚠️ 需避免许可证泄漏

依赖

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.22.0</version>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class RedissonConfig {

/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.2:6379");

//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}

}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Resource
private RedissonClient redissonClient;

@Test
void testRedisson() throws InterruptedException {
//获取(可重入),指定锁名称
RLock oneLock = redissonClient.getLock("oneLock");

//尝试获取锁,参数分别是: 获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = oneLock.tryLock(1, 30, TimeUnit.SECONDS); // 设置了超时时间不会有watch dog
if(isLock){
try {
System.out.println("执行业务");
TimeUnit.SECONDS.sleep(50);
}finally {
//释放锁
oneLock.unlock();
}
}
}
(1) 可重入锁 - RedissonLock\RLock

(2) 多节点 - MultiLock
1
2
3
4
5
6
7
8
9
10
@Test
void testRedissonMultiLock(){
// 分别从三个节点获取独立的锁
RLock lock1 = redissonClient1.getLock("lock1");
RLock lock2 = redissonClient2.getLock("lock1");
RLock lock3 = redissonClient3.getLock("lock1");
RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);
multiLock.lock();
multiLock.unlock();
}

4. 消息队列

5. Redis特殊数据结构的应用

GEO

BitMap

Hyperloglog

高级篇

Redis 主要通过三种模式实现分布式缓存,每种模式适用于不同的场景和需求。

特性 主从复制 (Replication) 哨兵模式 (Sentinel) 集群模式 (Cluster)
核心目标 数据备份、读写分离 高可用、自动故障转移 水平扩展、高可用、数据分片
数据分布 全量复制,所有节点数据相同 全量复制,所有节点数据相同 数据分片到16384个槽,每个节点负责部分槽
高可用性 手动故障转移 自动故障转移 自动故障转移
扩展性 读扩展(添加从节点) 读扩展(添加从节点) 读写扩展(添加主节点)
适用场景 数据备份、读多写少 对可用性有要求的业务 大数据量、高并发、需水平扩展

1. 主从模式

1.1 主从结构

1.2 Redis持久化

(1)RDB持久化

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。

快照文件称为RDB文件,默认是保存在当前运行目录

1
2
3
4
5
6
7
8
9
10
11
12
# 命令行执行
save # 由redis主进程来执行RDB,会阻塞所有命令
bgsave # 开启子进程执行RDB, 避免主进程受到影响

# redis 内部由触发RDB的机制,可以在redis.conf文件中找到,格式如下
save 900 1 # 表示900秒内,至少有一个key被修改,则执行bgsave
save "" # 表示禁用RDB

# 其他配置
rdbcompression yes # 是否被压缩
dbfilename dump.rdb # RDB文件名称
dir ./ # 文件保存的路径目录

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存;
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

  • 总结
    • RDB方式bgsave的流程
      • fork主进程得到一个子进程,共享物理内存空间
      • 子进程读取内存数据并写入新的RDB文件
      • 用新RDB文件替换旧RDB文件
    • RDB会在什么时候执行?save 60 1000代表什么?
      • 默认时服务停止时
      • 执行save、bgsave或者达到配置的条件时执行
      • 代表60s内至少执行1000次更改数据操作后,会自动进行bgsave操作
    • RDB的缺点?
      • 耗时
      • 占用内存

(2)AOF持久化

AOF全称为Append OnlyFile(追加文件)。Redis处理的每一个写命令都会记录在A0F文件,可以看做是命令日志文件。

AOF 默认是关闭的,需要修改redis.conf配置文件来开启

1
2
3
4
5
6
7
appendonly yes # 是否开启AOF功能,默认是no
appendfilename "appendonly.aof" # AOF文件名称

# AOF的命令记录频率配置
appendfync always # 每执行一次命令,立即记录到AOF磁盘文件 # 可靠性高,性能影响大
appendfync everysec # 默认方案,写命令写入缓冲区,每隔1秒写到磁盘AOF文件 # 性能适中,最多丢失1s数据
appendfync no # 写命令写入缓冲区,操作系统决定何时写入磁盘AOF文件 # 性能最好,可靠性最差

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

Redis也会在触发阈值时自动去重写AOF文件。值也可以在redis.conf中配置:

1
2
auto-aof-rewrite-percentage 100 # AOF文件比上次文件,增长超过多少百分比时触发AOF文件重写
auto-aof-rewirte-min-size 64mb # AOF文件体积最小多大以上才触发重写

1.3 数据同步原理

(1)全量同步

  1. slave执行replicaof命令,建立连接
    1. slave请求数据同步
    2. master判断是否时第一次请求同步,是第一次请求则slave获取master数据版本信息
    3. slave保持版本信息
  2. master异步执行bgsave,生成RDB
    1. master发送RDB文件给slave
    2. slave清空本地数据,加载RDB文件
  3. master记录生成RDB后的所有命令到repl_baklog
    1. 发送repl_baklog中的命令到slave
    2. 循环同步

master如何判断slave是否是第一次来同步数据的?

slave做数据同步,必须向master声明自己的replication idoffset, master才可以判断到底需要同步哪些数据。判断是否是第一次来,只需要判断master和slave的replid是否一致即可。

Replication Id: 是数据集的标识,id一致说明是同一个数据集。每一个master都有唯一的replid, slave 则会继承master的replid。

offset: 偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完全同步时也会记录当前同步的offset。如果slave的offset小于master的offset。说明slave数据落后于master,需要更新。

(4)增量同步

  1. slave 重启
    1. slave请求数据同步
    2. 判断replid一致,回复slave:continue
  2. 去repl_baklog中获取slave offset后的数据
    1. 发送offset后的命令

repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。

(3)主从同步优化

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时写入RDB文件的磁盘IO,直接将RDB IO流直接发送给slave。
  • Redis单节点上的内存占用要太大,减少RDB导致的过多磁盘IO。
  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主从-从链式结构,减少master压力

(4)总结

简述全量同步和增量同步的区别?

  • 全量同步:master将完整的内存数据生成RDB,发送RDB文件到slave。在生成RDB文件之后的命令操作,则记录在repl_baklog, 逐个发送给slave;
  • 增量同步:slave提交自己的offset到master,master获取repl_baklog中offset偏移量之后的命令给slave

什么时候执行全量同步?

  • slave初次同步
  • slave的偏移量offset超过了repl_baklog的大小

什么时候执行增量同步?

  • slave从故障中恢复并且offset在repl_baklog中能找到时

2. 哨兵模式

主从模式中slave宕机后恢复可以从master中同步数据,如果是master宕机呢?

2.1 哨兵的作用和原理

Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:

  • 监控:Sentinel会不断检查您的master和slave是否按预期工作
  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也会以新的master为主,实现主从切换。
  • 通知: Sentinel充当Redis客户端的服务发现来源。当Redis集群发送故障转移时,会将最新的信息推送到Redis客户端。

(1)服务状态监控

Sentinel基于心跳机制检测服务状态,每隔一秒向集群的每个实例发送一个ping命令:

  • 主观下线:如果某个sentinel节点发现某个实例未在规定的时间响应,则认为该实例主观下线。
  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例下线,则该实例客观下线。 quorum最好超过实例数量的一半。

(2)选举master

一旦发现master故障,sentinel需要在slave中选择一个作为新的master。

  • 断开时间:首先判断slave节点与master节点断开时间的长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
  • 优先级:然后判断slave节点的slave-priority值,越小的优先级越高,如果是0则永不参与选举。
  • 偏移值offset:如果slave-priority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高。
  • 运行id:最后判断slave节点的运行id大小,越小优先级越高(运行id是redis自动生成的id,也就是说这里随机挑选一个slave作为master)

(3)故障转移

当选中了一个slave作为新的master节点之后,需要进行故障转移。

  • sentinel给备选的slave发送slaveof no one命令,让该节点成为master
  • sentinel给所有其他slave发送slaveof 192.168.2.2 7002命令,让其他slave成为新master的从节点,开始从新的master上同步数据。
  • 最后,sentinel将故障节点标记未slave,当故障节点恢复后会自动成为新master节点的slave节点。

(4)总结

  • sentinel的三个作用是什么?

    • 状态监控
    • 故障修复
    • 故障恢复通知
  • Sentinel如何判断一个redis实例是否健康?

    • 心跳机制:基于心跳机制,每隔一秒向每个实例发送一个ping命令
    • 主观判断:如果某个实例没有在规定时间内响应,则认为该节点主观下线。
    • 客观判断:如果超过指定数量的sentinel都认为该节点主观下线,则该实例客观下线。
  • 故障转移步骤有哪些?

    • master选举
    • sentinel给备选slave发送slaveof no one命令,让该节点成为master
    • sentinel给其他slave节点发送该节点的ip + 端口信息,让其他slave节点成为新master节点的从节点。
    • sentinel把故障master标记为slave,恢复后会成为新master的slave节点。
  • 优点

    • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。

    • 主从可以自动切换,系统更健壮,可用性更高。

  • 缺点

    • 具有主从模式的缺点,每台机器上的数据是一样的,内存的可用性较低。

    • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

2.2 搭建哨兵集群

2.3 RedisTemplate的哨兵模式

在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新信息。Spring的RedisTemplate底层利用了lettuce实现了节点的感知和自动切换。

  1. 在pom文件中引入redis的starter依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 在配置文件中配置sentinel信息
1
2
3
4
5
6
7
8
spring:
redis:
sentinel:
master: mymaster # 指定master名称
nodes: # 指定redis-sentinel集群信息
- 192.168.56.2:27001
- 192.168.56.3:27001
- 192.168.56.4:27001
  1. 配置主从读写分离
1
2
3
4
@Bean
public LettuceClientConfigurationBuilderCustomer configurationBuilderCustomer(){
return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

这里ReadFrom是配置Redis的读写策略。

  • MASTER:从节点读取
  • MASTER_PREFERRED: 优先从master节点读取
  • REPLICA: 仅从slave节点读取
  • REPLICA_PREFERRED: 优先从slave节点读取

3. 分片集群模式

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

使用分片集群可以解决上述问题,分片集群特征:

  • 集群中有多个master,每个master保存不同数据
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此的健康状态
  • 客户端请求可以访问集群中任意节点,最终都会被转发到正确节点。

3.1 搭建分片集群

redis.conf

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
port 6379
# 开启集群功能
cluster-enabled yes

# 集群的配置文件名称,不需要我们创建,由redis自己维护,只需要指明其位置即可
cluster-config-file /tmp/6379/nodes.conf

# master节点心跳失败的超时时间
cluster-node-timeout 5000 # 5s

# 持久化文件存放目录
dir /tmp/6379

# 绑定地址
bind 0.0.0.0 # 任何ip都可以访问本节点

# 让redis后台运行
daemonize yes

# 注册的实例ip
replica-announce-ip 192.168.56.2 # master ip

# 保护模式
protected-mode no # 取消用户名和密码校验

# 数据库数量
database 1

# 日志
logfile /tmp/6379/run.log
1
2
3
4
5
6
7
8
redis-cli --cluster create / 创建集群
--cluster-replicas 1 / # 副本数量为1
192.168.56.2:7001 / # 自动判断: 副本数量为1,说明一主一从,有6个节点,则前面3个是master,后面3个是slave
192.168.56.2:7002 /
192.168.56.2:7003 /
192.168.56.2:8001 /
192.168.56.2:8002 /
192.168.56.2:8003
1
2
# 查看集群状态
redis-cli -p 7001 cluster nodes

3.2 散列插槽

Redis会把每一个master节点映射到0~16383个插槽上(hash slot)上。

数据的key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:

  • key中包含{}, 且{}中至少包含一个字符, {}中的部分是有效部分。
  • key中不包含{},整个key都是有效部分

例如:key是num,那么就根据num计算,如果是{momo}num, 则根据momo计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。

  • 总结
    • Redis如何判断某个key应该在哪个实例?
      • 集群初始化时会给每个节点分配不同的插槽值 0 ~ 16383
      • 通过CRC-16算法计算hash值,用其对16384取余计算插槽位置
      • 通过插槽位置获取对应的master节点ip和端口
    • 如何将同一类数据固定的保存在同一个Redis实例中?
      • 计算某一类数据相同的有效部分,例如key都以{typeId}作为前缀

3.3 集群伸缩

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
root@e8b5881fcf9b:/data# redis-cli --cluster help
Cluster Manager Commands:

reshard <host:port> or <host> <port> - separated by either colon or space
--cluster-from <arg>
--cluster-to <arg>
--cluster-slots <arg>
--cluster-yes
--cluster-timeout <arg>
--cluster-pipeline <arg>
--cluster-replace
add-node new_host:new_port existing_host:existing_port # 添加新节点需要标明原有的节点
--cluster-slave # 默认添加后是主节点,使用此标识表示添加的节点为从节点
--cluster-master-id <arg>
del-node host:port node_id
call host:port command arg arg .. arg
--cluster-only-masters
--cluster-only-replicas
set-timeout host:port milliseconds
import host:port
--cluster-from <arg>
--cluster-from-user <arg>
--cluster-from-pass <arg>
--cluster-from-askpass
--cluster-copy
--cluster-replace
backup host:port backup_directory
help
  • 案例
    • 向集群中添加一个新的master节点,并向其存储num=10
      • 启动一个新的redis实例,端口7004
      • 添加7004到之前的集群,并作为一个master节点 redis-cli --cluster add-node 192.168.56.2:7004
      • 给7004节点分配插槽,使得num这个key可以存储到7004 redis-cli --cluster reshard 192.168.56.2:7004

3.4 故障转移

当集群中有一个master节点宕机会发生什么?

  • 首先是该实例与其他实例断开连接
  • 确定是下线后,自动提升一个slave作为新的master节点

如何实现数据迁移?

  • 利用cluster failover命令可以手动让集群中的某个master节点宕机,切换到cluster failover命令的这个slave节点。实现无感知的数据迁移。
    1. slave节点告诉master,该master节点拒绝任何客户端请求
    2. master返回当前的数据offset给slave
    3. 等到数据offset和master一致,开始故障转移
    4. slave标记自己为master,广播故障转移结果

3.5 RedisTemplate访问分片集群

RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用步骤与哨兵模式基本一致。

  1. 引入redis的start依赖
  2. 配置分片集群的地址
  3. 配置读写分离

与哨兵模式相比,分片集群需要配置每个节点的信息

1
2
3
4
5
6
7
8
9
10
spring:
redis:
cluster:
nodes: # 指定每一个节点的信息
- 192.168.56.2:7001
- 192.168.56.2:7002
- 192.168.56.2:7003
- 192.168.56.2:7004
- 192.168.56.2:7005
- 192.168.56.2:7006

4. 多级缓存

4.1 JVM进程缓存

1
2
Cache<String,String> cache = Caffeine.newBuilder().build();
cache.put("key","value");

4.2 Nginx缓存 - OpenResty

4.3 Redis缓存 - Lua脚本

(1)初识lua

lua是一种轻量小巧的脚本语言,用标准的c语言编写并以源代码形式开发。设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

官网:https://www.lua.org/

HelloWorld

1
2
3
4
# cenos 默认安装lua编译器
touch hello.lua
print("hello world!")
lua hello.lua

(2)变量和循环

  • 变量
    • nil : 无效值,在条件表达式中表示false
    • boolean:false 和true
    • number:双精度类型的实浮点数
    • string:单引号或双引号包裹的字符串
    • function:由C或者lua编写的函数
    • table:表/关联数组, 数组的索引可以是数字、字符串、表类型。类似于Map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local str = 'hello' -- local表示申请局部变量

-- 声明数组 key为索引的table
arr = {'java','py','lua'}

-- 声明table,map结构
map = {name='jack', age=21}

-- 访问数组, 从1开始
print(arr[1])

-- 访问map
print(map['name'])
print(map.name)

-- 字符串凭借
1
2
3
4
5
6
7
8
9
10
11
-- 遍历数组
arr = {'java','python','lua'}
for index,value in ipairs(arr) do
print(index,value)
end

-- 遍历map
map = {name='jack',age=20}
for key,value in pairs(map) do
print(key,value)
end

(4)条件控制和函数

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
-- 定义函数
function 函数名(arg1,arg2,...)
--函数体
return 返回值
end

-- 例子
funtion printArr(arr)
for index,value in pairs(arr) do
print(index,value)
end
end

-- 条件控制
if(expr)
then
-- true
else
-- false
end


if(not arr) then
return nil
else
printArr(arr)
end

4.4 数据库监听 - Canal 客户端伪装从库

5. Redis应用最佳实践

5.1 Redis键值设计

(1)优雅的key结构

redis的key虽然可以自定义,单最好遵循下面的几个最佳实践约定:

  • 基本格式:业务名称(功能):数据名(类名):id(对象)
    • 可读性强
    • 避免key冲突
    • 方便管理
  • 长度不超过44字节
    • 节省内存:key是string类型,底层编码包含int, embstr和raw三种. embstr在小于44字节使用,采用连续内存空间,无内存碎片
  • 不包含特殊字符

(2)拒绝BigKey

BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:

  • 一个String类型的Key,它的值为10k(数据过大);
  • 一个List类型的Key,它的列表数量为20000个(列表数量过多);
  • 一个ZSet类型的Key,它的成员数量为10000个(成员数量过多);
  • 一个Hash格式的Key,它的成员数量虽然只有1000个但这些成员的value总大小为100MB(成员体积过大)
  • 推荐值:
    • 单个key的value小于10KB
    • 对于集合类型的key,建议元素数量小于1000
BigKey的危害
  • 网络阻塞
    对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
  • 数据倾斜
    BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
  • Redis阻塞
    对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞
  • CPU压力
    对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
如何发现BigKey
  • redis-cli –bigkeys
    利用redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key
  • scan扫描 (推荐)
    自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)
  • 第三方工具(推荐)
    利用第三方工具,如 Redis-Rdb-Tools分析RDB快照文件,全面分析内存使用情况
  • 网络监控(云服务推荐)
    自定义工具,监控进出Redis的网络数据,超出预警值时主动告警

如何删除BigKey
Redis在4.0后提供了异步删除的命令unlink: UNLINK key

(3)恰当的数据类型

(4)总结

  • key的最佳实践

    • 固定格式业务名:类名:id

    • 足够简短:44字节

    • 不包含特殊字符

  • value的最佳实践

    • 合理的数据拆分,拒绝bigkey
    • 选择合适的数据类型
    • Hash结构的entry数量不要超过1000(默认500)
    • 设置合理的过期时间

5.2 批处理优化

Redis提供了很多Mxxx的命令,可以实现批量插入数据,例如:、

  • mset
  • hmset

注意⚠:不要再一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞

(1)Pipline

MSET虽然可以批处理,但是只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能

1
2
3
4
5
6
7
Pipline pipline = jedis.pipelined(); // 创建管道
for(int i = 1; i < 10000; i++){
pipeline.set("test:key_"+i);
if(i % 500){
pipeline.sync();
}
}
  • 批处理的方案
    • 原生M操作
    • Pipeline处理
  • 注意事项
    • 批处理不建议一次携带太多命令
    • Pipeline的多个命令之间不具备原子性

(2)集群下的批处理

如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。

推荐使用并行slot

5.3 服务端优化

(1)持久化配置

Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

  • 用来做缓存的Redis实例尽量不要开启持久化功能
  • 建议关闭RDB持久化功能,使用AOF持久化
  • 利用脚本定期在slave节点做RDB,实现数据备份
  • 设置合理的rewrite阈值,避免频繁的bgrewrite
  • 配置no-appendfsync-on-rewrite=yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞

部署有关建议:

  • Redis实例的物理机要预留足够内存,应对fork和rewrite
  • 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
  • 不要与CPU密集型应用部署在一起
  • 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

(2)慢查询

慢查询: 在Redis执行时耗时超过某个值的命令,称为慢查询。

慢查询的阈值可以通过配置指定:

  • slowlog-log-slower-than: 慢查询阈值,单位是微秒。默认是10000,建议1000

慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定

  • slowlog-max-len: 慢查询日志(本质是一个队列)的长度。默认是128,建议1000

修改以上两个配置可以在reids命令行中执行

1
2
3
4
5
config set slowlog-log-slower-than 1000
config get slowlog-log-slower-than
confit set slowlog-max-len 1000
config get slowlog-max-len

查看慢查询日志列表:

  • slowlog len:查询慢查询日志长度 SLOWLOG LEN
  • slowlog get[n]:读取n条慢查询日志 SLOWLOG GET
  • slowlog reset:清空慢查询列表 SLOWLOG RESET

(3)命令及安全配置

Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞
漏洞重现方式:https://cloud.tencent.com/developer/article/1039000

漏洞出现的核心原因:

  • Redis未设置密码
  • 利用了Redis的config set命令动态修改Redis配置
  • 使用root账号权限启动redis

避免安全漏洞:

  • 不要使用root用户启动redis

  • 开启防火墙

  • Redis一定要设置密码

  • 尽量不使用默认的端口

  • 禁止线上使用下面命令: keys, flushall, flushdb, config set等。 可以利用rename-command禁用

  • bind:限制网卡,禁止外网访问

(4)内存配置

当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、OPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。

Redis提供了一些命令,可以查看到Redis目前的内存分配状态

  • info memory
  • memory xxx

内存缓冲区配置:

  • 复制缓冲区:主从复制的repl_baklog_buf,如果太小可能会导致频繁的全量复制,影响性能,通过repl-backlog-size来设置,默认1M
  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓存区。无法设置容量上限。
  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置。

5.4 集群最佳实践

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  • 集群完整性问题
    • cluster-require-full-coverage yes 这样的配置是插槽全覆盖,一旦某个插槽对应服务不可用,则整个集群无法使用
    • 建议将其配置为no,以保证高可用特性
  • 集群带宽问题
    • 集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:
      • 插槽信息
      • 集群状态信息:集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。
    • 解决途径:
      • 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
      • 避免在单个物理机中运行太多Redis实例
      • 配置合适的cluster-node-timeout(超时下线时间)值
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • lua和事务问题

单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。

原理篇

1. Redis常见数据类型和底层结构

1.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
# String
set `key` `value`
setnx `key` `value` # 不存在才set
setex `key` `value` `ttl`
incrby `key` `increment` # 自增

# Hash 哈希表
hset `key` `field` `value` `field` `value`
hget `key` `field`
hsetnx `key` `value` # 不存在才set

# List 双向链表
LPUSH `key` `element1` `element2` ... # 从左侧插入一个或多个元素
LPOP `key` # 移除左侧第一个元素,没有返回null
RPUSH `key` `element1` `element2` ...
RPOP `key`
LRANGE `key` `start` `end` # 返回start到end之间的元素

# Set 无序集合
SADD `key` `member1` `member2` # 添加
SREM `key` `member1` `member2` # 删除
SCARD `key` # 返回set中元素的个数
SISMEMBER `key` `membern` # 判断是否是key成员
SmemberS `key` # 查询key的所有成员
SINNER `key1` `key2` # 获取交集
SDIFF `key1` `key2` # 求差集, key1有,key2没有
SUNION `key1` `key2` # 求并集

# Zset/SortedSet ### 跳表 + Hash表, 有序集合,查询效率高
ZADD `key` `score1` `member1` `score2` `member2` ... # 添加一个或多个元素到sorted set,如果存在更新score
ZREM `key` `member` # 删除
ZSCORE `key` `member` # 获取元素的score
ZRANK `key` `member` # 获取元素排名
ZCARD `key` # 获取元素个数
ZCOUNT `key` `min` `max` # 求指定范围score的元素个数
ZINCRBY `key` `increment` `member` # 对指定member自增
ZRANGE `key` `min` `max` # 获取指定排名范围的元素
ZRANGEBYSCORE `key` `min` `max` # 获取指定score范围内的元素

各种数据类型底层结构:

数据类型 典型使用场景 底层数据结构 (演进)
String (字符串) 缓存对象、常规计数(点赞/访问量)、分布式锁、共享 Session **SDS (Simple Dynamic String)**。根据值的类型和长度,编码可为 intembstr(短字符串)或 raw(长字符串)。
Hash (哈希) 缓存对象(尤其适合部分更新)、购物车 ListPack (Redis 7.0+) 或 HashTable。元素少时用 ListPack 节省内存,多时转 HashTable 保证性能。
List (列表) 消息队列、时间轴/文章列表、待办事项 QuickList (Redis 3.2+),一种由多个 ListPack (Redis 7.0+) 或 ZipList (Redis 7.0 前) 节点组成的双向链表。
Set (集合) 标签系统、共同好友/关注、抽奖去重 IntSet (整数集合,元素均为整数且数量少时) 或 HashTable
ZSet (有序集合) 排行榜、实时热搜、带权重的消息队列 ListPack (Redis 7.0+) 或 SkipList (跳跃表) + HashTable 的组合。ListPack 用于小数据集,大数据集用跳表保证范围查询效率,哈希表保证点查询速度。
BitMap (位图) 用户签到、在线状态、二值统计 基于 String 类型实现。
HyperLogLog 大规模数据集的近似去重统计(如 UV) 特殊的概率算法数据结构。
GEO (地理空间) 附近的人、门店定位 底层使用 ZSet 实现。
Stream (流) 消息队列(支持消费者组和消息回溯) **Radix Tree (基数树)**。

1.2 动态字符串SDS

Redis通过C语言编写, 但是Redis没有使用c语言中的字符串,原因:

  • 获取字符串的长度需要计算
  • 非二进制安全 (C语言的字符串之所以被认为是二进制不安全的,是因为它依赖空字符 \0作为字符串结束的唯一标识,这导致它无法正确存储和处理包含 \0的二进制数据。)
  • 不可修改

Redis构建了一种新的字符串结构, 称为简单动态字符串(Simple Dynamic String) 简称SDS.

1
set name "jack" # 底层创建两个SDS, 包含name的SDS,包含jack的SDS

SDS是一个C语言的结构体:

1
2
3
4
5
6
struct __atrribute__((__packed)) sdshdr8 {
uint8_t len; // buf 已保存的字符串字节数,不包含结束字符。uint16, uint32
uint8_t alloc; // buf 申请的总字节数, 不包含结束字符
unsigned char flags; // 不同SDS的头类型,用来控制SDS的头大小
char buf[]; // SDS中保存的数据
}

SDS可以动态扩容

结构体类型 lenalloc字段类型 所能表示的最大长度 适用场景
sdshdr5 (未直接使用长度字段) 31 字节 (2^5 - 1) 极短的字符串标识
sdshdr8 uint8_t(1字节) 255 字节 (2^8 - 1) 较短的字符串
sdshdr16 uint16_t(2字节) 65,535 字节 (64KB) 中等长度字符串
sdshdr32 uint32_t(2字节) 4,294,967,295 字节 (4GB) 长字符串
sdshdr64 uint64_t(8字节) 极大 (理论值) 超长字符串 (Redis 规范限制 512MB)

优点:

  • 获取字符串长度的时间复杂度为O(1)
  • 支持动态扩容
  • 减少内存分配次数
  • 二进制安全
特性 C语言字符串 Redis SDS (简单动态字符串)
结束标识 依赖空字符 \0 使用独立的 len 属性记录长度
长度获取 需遍历查找 \0,时间复杂度 O(n) 直接访问 len属性,时间复杂度 O(1)
二进制安全 不安全 (无法处理内含 \0的数据) 安全 (可存储任何二进制数据)
缓冲区溢出 高风险 (操作不检查边界,如 strcpy) 低风险 (API会自动检查边界和扩容)
内存分配 频繁 (每次修改都可能需重新分配) 高效 (采用空间预分配惰性空间释放策略)

1.3 IntSet

IntSet是Redis中set集合的一种实现方式, 基于整数数组来实现, 并且具备长度可变, 有序等特性.

1
2
3
4
5
6
// InstSet结构
typedef struct intset {
uint32_t encoding; // 编码方式,16(short) 32(int) 64(long)
uint32_t length; // 元素个数
int8_t contents[]; // 整数数组
} intset;

扩容流程:

  1. 升级编码格式
  2. 倒序依次拷贝
  3. 将待添加的元素放在数组末尾

总结:

  • Redis会确保Inset中的元素唯一,有序
  • 具备类型升级机制, 可以节省内存空间
  • 底层采用二分查找方式来查询

1.4 Dict

我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的,

Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)

Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。
Dict在每次新增键值对时都会检查负载因子(LoadFactor=used/size),满足以下两种情况时会触发哈希表扩容

  • 哈希表的 LoadFactor >=1,并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;
  • 哈希表的 LoadFactor>5;

Dict除了扩容以外,每次删除元素时,也会对负载因子做检查,当LoadFactor<0.1 时,会做哈希表收缩

总结:

  • Dict的结构
    类似java的HashTable,底层是数组加链表来解决哈希冲突Dict包含两个哈希表,ht[0]平常用,ht[1]用来rehash
  • Dict的伸缩
    • 当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
    • 当LoadFactor小于0.1时,Dict收缩扩容大小为第一个大于等于used+1的2”
    • 收缩大小为第一个大于等于used 的2^n
    • Dict采用渐进式rehash,每次访问Dict时执行一次rehash
    • rehash时ht[0]只减不增,新增操作只在ht[1]执行,其它操作在两个哈希表

1.5 ZipList

zipList 是一种特殊的“双端链表”,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为 0(1)。

  • zlbytes:总字节数
  • zltail: 尾节点偏移
  • zllen:列表中节点数量
  • entry:数据节点, 长度不定
    • previous_entry_length:前一个节点的长度
    • encoding:编码属性,记录数据类型以及长度
    • contens:保存节点的数据
  • zlend:0xFF 结束标识

ZipList特性:

  • 压缩列表的可以看作是一种连续内存空间的”双向链表“
  • 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址, 内存占用低
  • 如果列表数据过多,导致链表过长,可能影响查询性能
  • 新增或者删除较大的数据时,可能引发连锁更新的问题。

1.6 QuickList

QuickList特点:

  • 是一个节点为ZipList的双端链表
  • 节点采用ZipList,解决了传统链表内存占用的问题
  • 控制了ZipList大小,解决了连续内存空间申请效率的问题
  • 中间节点可以压缩,进一步节省内存

1.7 SkipList

Skip List首先是链表,但是具有:

  • 元素按照升序排列存储
  • 节点可能包含多个指针,指针跨度不同

SkipList特点:

  • SkipList是一个双向链表, 每个节点都包含score和ele值
  • 节点按照score排序,score则按ele字典排序
  • 每个节点都可以包含多层指针,层数是1~32之间的随机数
  • 不同层指针到下一个节点的跨度不同,层数越高,跨度越大
  • 增删改查效率和红黑树基本一致,实现方式更简单

1.8 RedisObject

Redis中的任意数据类型的键和值都会被封装为一个RedisObject, 也叫做Redis对象

1
2
3
4
5
6
7
typeof struct redisObject{
unsigned type:4; //对象类型, String, hash, list, set, zset
unsigned encoding:4; // 编码方式: 11种,占4bit
unsigned lru:LRU_BITS; // 24 表示该对象最后一次被访问的时间
int refcount; // 引用计数器,
void *ptr; // 指针,指向对象的空间
} robj;

2. Redis网络模型

2.1 用户空间和内核空间

为了避免用户应用导致冲突甚至内核崩溃,用户应用与操作系统内核是分离的。

  • 进程的寻址空间会划分为:内核空间、用户空间
    • 用户空间:只能执行受限的命令(Ring3), 而且不能直接调用系统资源,需要通过内核提供的接口来访问
    • 内核空间:可以执行特权指令(Ring0),调用一切系统资源
  • Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区
    • 写数据时,用户空间缓冲区数据拷贝到内核空间缓冲区,然后写入设备
    • 读数据时,要从设备读取到内核空间,然后拷贝到用户缓冲区

2.2 阻塞IO

在《UNIX网络编程》一书中,总结归纳了5种IO模型

  • 阻塞IO (Blocking IO)
  • 非阻塞IO (Nonblocking IO)
  • IO 多路复用 (IO Multiplexing)
  • 信号驱动IO (SignalDriven IO)
  • 异步IO (AsynchronousIO)

阻塞IO: 多任务等待

2.3 非阻塞 IO

非阻塞IO:多任务时,不等待,直接放弃任务或去做其他的任务。

结合多路复用

2.4 IO 多路复用

无论是阻塞I0还是非阻塞10,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:

  • 如果调用recvfrom时,恰好没有数据,阻塞10会使进程阻塞,非阻塞10使CPU空转,都不能充分发挥CPU的作用
  • 如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据

比如服务端处理客户端Socket请求时,在单线程情况下,只能依次处理每一个socket,如果正在处理的socket恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有其它客户端socket都必须等待,性能自然会很差。

  • 文件描述符(File Description):简称FD,是一个从0 开始递增的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。
  • IO多路复用: 就是利用单个线程同时监听多个FD, 并且在某个FD可读, 可写时得到通知, 从而避免无效的等待, 充分利用CPU资源.
    • 复用的就是线程
    • 监听FD的方式, 通知的方式Linux有多种实现
      • select: 通知有就绪FD, 进程轮询查找
      • poll: 通知有就绪FD, 进程轮询查找
      • epoll: 把已就绪的FD通知给用户进程

当FD有数据可读时,我们调用epoll_wait就可以得到通知。但是事件通知的式有两种:

  • LevelTriggered:简称LT。当FD有数据可读时,会重复通知多次,直至数据处理完成。是Epoll的默认模式。
  • EdgeTriggered:简称ET。当FD有数据可读时,只会被通知一次,不管数据是否处理完成。

2.5 信号驱动IO

信号驱动IO是与内核建立SIGIO的信号关联并设置回调, 当内核有FD就绪时, 会发出SIGIO信号通知用户, 期间用户应用可以执行其他业务, 无需阻塞等待.

当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出. 而且内核空间与用户空间的频繁信号交互性能也会变低.

2.6 异步IO

异步I0的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。

2.7 Redis网络模型

Redis 到底是单线程还是多线程?

  • 如果是Redis核心业务数据的增删改查, 答案是单线程
  • 如果是整个Redis服务, 答案是多线程

为什么Redis要选择单线程?

  • 抛开持久化不谈, Redis是纯内存操作, 执行速度非常快, 它的性能瓶颈是网络延迟而不是执行速度, 因此多线程并不会带来巨大的性能提升.
  • 多线程会导致过多的用户态和内核态的频繁切换, 带来性能损耗
  • 多线程会带来线程安全问题, 会引入线程锁的这样的安全机制, 也会带来性能损耗

Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的
高性能事件库API库 AE:

Redis中多线程使用场景:

  • 命令解析
  • 响应结果输出

3. Redis内存淘汰策略

内存淘汰: 当Redis中内存使用达到设定的阈值时,Redis主动挑选部分key删除,以释放更多内存的过程。

LRU: 最少最近使用, 访问时间越旧,则删除

LFU: 最少频率使用, 访问频率越低,则删除

1
2
# reids.conf
maxmemory-policy noeviction

常见面试题

Redis常用的数据类型有哪些?Zset的底层数据结构是什么?

Zset 底层数据结构ZipList和SkipList?

redis集群有几种模式?分别讲讲这些集群模式的基本原理是什么?

如何处理Redis集群数据倾斜?

Redis数据倾斜问题也是面试热门场景技术题。在正常情况下,各数据分片节点的Key数量是均匀分布的,同时内存使用率、CPU使用率等性能指标也是相近的。一般是在使用Redis的过程中,设计考虑不周、不规范的数据写入及突发的访问量,造成redis个别的节点数据量倾斜或数据访问倾斜,最终引起数据倾斜。常见倾斜场景有:

(1)内存倾斜。一般由于大key问题或者使用hash Tages集中到某个节点。
(2)带宽倾斜。大key 热key 高消耗命令造成 访问节点占用带宽
(3)CPU倾斜。大key 热key 高消耗命令造成CPU使用率偏高

明白了场景和问题原因之后,解决方案主要有:
(1)减少大key的使用,或者对大key进行拆分成多个 hash key
(2) 禁止使用高消耗命令,或者流量高峰期禁止使用。这个我们之前有讲过。
(3) 减少Hash Tags的使用
(4)增加本地缓存,减少热key对Redis的压力
(5)提高内存配置和带宽等资源,从硬件角度减少数据倾斜带来的问题