Redis大全 (持续更新)
2024-06-19 10:31:23 5 举报
AI智能生成
有问题在评论区讨论 Redis基础、底层实现、应用
作者其他创作
大纲/内容
简介
NoSQL
Not only SQL,非关系型数据库
key-value模式
特点
不遵循SQL标准
不支持ACID
远超SQL性能
应用场景
高并发读写
海量数据读写
对数据可高扩展
不适应
需要事务支持
处理复杂的关系
常见
Mamecache
Redis
MongoDB
入门
指令
keys *
查看当前库所有key
exists KEY
KEY是否存在
type KEY
类型
del KEY
删除
unlink KEY
选择非阻塞删除,仅从keyspace中删除,后续删除异步进行
expire KEY TIME
设置过期时间,单位秒
ttl KEY
还有多少时间过期,-2已过期,-1永不过期
select DB
切换库
dbsize
db中key的数量
flushdb
清空当前库
flushall
清空所有库
基本数据类型
String
简介
二进制安全的
value最多512M
数据结构是SDS,类似Java的ArrayList,可以动态扩容
指令
set KEY VALUE
get KEY
append KEY VALUE
追加
strlen
setnx KEY VALUE
只有在key不存在时设置
incr KEY
数字类型加1
原子操作
incrby KEY NUMBER
decr KEY
减一
原子操作
decrby KEY NUMBER
mset KEY1 VALUE1 KEY2 VALUE2...
同时操作多个
mget
msetnx
只有key都不存在才成功
getrange KEY START END
setrange
setex KEY TIME VALUE
同时设置过期时间
getset KEY VALUE
设置新值后返回旧值
数据结构
List
简介
底层
quicklist
元素较少时ziplist
存储空间是连续的
元素较多时由多个ziplist用链表形式相连
指令
lpush/rpush KEY VALUE1 VALUE2...
从左边/右边插入
lpop/rpop
吐出一个值,列表长度为0时,列表消失
rpoplpush KEY1 KEY2
从KEY1中右边吐出一个放入KEY2左边
lrange KEY START END
从左到右获取索引START到END的元素
lindex KEY INDEX
从左开始获取第INDEX的元素
llen KEY
长度
linsert KEY before/after VALUE1 VALUE2
在VALUE1的前面/后面插入VALUE2
lrem KEY NUMBER VALUE
从左开始删除NUMBER个VALUE值
lset KEY INDEX VALUE
将INDEX位置换成VALUE
应用场景
消息队列
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。
如何满足消息保序需求?
List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。
在生产者往 List 中写入数据时,List 并不会主动地通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停地调用 RPOP 命令(比如使用一个while(1)循环)。如果有新消息写入,RPOP命令就会返回结果,否则,RPOP命令返回空值,再继续循环。
Redis提供了 BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。
如何处理重复的消息?
1、每个消息都有一个全局的 ID。
2、消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。
2、消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。
List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一ID
如何保证消息可靠性?
当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。
为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
Redis 的 List 和 Stream 两种数据类型,就可以满足消息队列的这三个需求。
List 作为消息队列有什么缺陷?
List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。
要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。
要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。
数据结构
Set
简介
无序、单一
底层
哈希表
指令
sadd KEY1 VALUE1 VALUE2...
添加多个
smembers KEY
显示所有值
sismember KEY VALUE
是否存在
scard KEY
集合元素个数
srem KEY VALUE1 VALUE2...
删除
spop
随机吐出
srandmember KEY NUMBER
随机获取NUMBER个值,不删除
smove KEY1 KEY2 VALUE
value从KEY1转移到KEY2
sinter KEY1 KEY2
交集
sunion KEY1 KEY2
并集
sdiff KEY1 KEY2
差集
数据结构
Hash
指令
hset KEY FIELD VALUE
向KEY中加入一个字段为FIELD值为VALUE的元素
hget KEY FIELD
hmset KEY1 FIELD1 VALUE1 KEY2 FIELD2 VALUE2...
写入多个
hexists KEY FIELD
hkeys KEY
hvals KEY
hincrby KEY FIELD NUMBER
KEY的FIELD字段加上NUMBER
field不存在也能加
hsetnx KEY FIELD VALUE
不存在的FIELD存值,存在就不存
数据结构
数量少时ziplist,数量多时hashtable
应用场景
缓存对象
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
购物车
以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素
数据结构
Zset
简介
有序集合,数据不能重复,每个值都有score,score可以重复
指令
zadd KEY SCORE1 VALUE1 SCORE2 VALUE2
zrange KEY START END [withscores]
zrevrangebyscore KEY MAXSCORE MINSCORE [withscores] [limit offset count]
返回有序集合中,所有score介于MINSCORE和MAXSCORE的成员(包括这两个值),从小到大排序
zincrby KEY NUMBER VALUE
zcount KEY MINSCORE MAXSCORE
zrem KEY VALUE
zrank KEY VALUE
返回排名
数据结构
BitMap
简介
本身还是string,方便进行位操作
指令
setbit KEY OFFSET VALUE
getbit KEY OFFSET
bitcount KEY [START END]
计算这个KEY中有多少个1
START和END
start和end表示第几个比特
当END为负数时,表示倒数第几个比特
例
00000000 00000001 00000010 00000111
0 1 2 3
f(start, end)=result
f(0,0)=0
从左到右从第0个比特到第0个比特有0个1
f(0,1)=1
从左到右第0个比特到第一个比特有1个1
f(0,2)=2
f(0,3)=5
f(start, end)=result表示bitcount key start end的输出是result
bitop and KEY1 KEY2
and与操作,可替换为or(或) not(非) xor(异或)
返回1的数量
实现
bitMap本身是用string类型作为底层数据结构
string底层是byte数组
HyperLogLog
简介
提供不精确的去重计数
HyperLogLog基于概率完成的,误差率0.81%
占据12k内存,可以计算将近2^64个不同元素的基数,对比Set,HyperLogLog就很省空间
命令
PFADD key element [element...]
PFCOUNT key [key...]
PFMERGE destkey sourcekey [sourcekey...]
内部实现
todo
GEO
主要用于存储地理位置信息,并对存储的地理位置信息进行操作
指令
# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
实现
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
Stream
Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
指令
XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
XLEN :查询消息长度;
XREAD:用于读取消息,可以按 ID 读取数据;
XDEL : 根据消息 ID 删除消息;
DEL :删除整个 Stream;
XRANGE :读取区间消息
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:
XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
XACK 命令用于向消息队列确认消息处理已完成;
XLEN :查询消息长度;
XREAD:用于读取消息,可以按 ID 读取数据;
XDEL : 根据消息 ID 删除消息;
DEL :删除整个 Stream;
XRANGE :读取区间消息
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:
XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
XACK 命令用于向消息队列确认消息处理已完成;
Redis 基于 Stream 消息队列与专业的消息队列有哪些差距?
一个专业的消息队列,必须要做到两大块:
消息不丢。
消息可堆积。
消息不丢。
消息可堆积。
Redis Stream 消息会丢失吗?
生产阶段是否会丢失
生产者消息发至Redis后,成功会返回Ack,如果生产者没收到ACK则可以再发一条,因此不会丢失
消费阶段是否会丢失
在消费者接收到数据后,Redis会将数据保存在PendingList里,只有消费者发送ACK后才会从PendingList中删除,因此不会丢失
中间件阶段是否会丢失
AOF每秒刷盘可能丢失1秒内的数据
主从同步是异步的,主从切换也可能导致丢失数据
Redis
发布订阅
subscribe CHANNEL
订阅
publish CHANNEL CONTEXT
发布
事务
简介
事务是一个单独的隔离操作,事务中的所有命令都会被序列化,按顺序执行,在事务执行过程中不会被其他客户端发送过来的命令打断
不保证原子性,没有隔离级别的概念,单独的隔离操作
指令
multi
组队
exec
执行队列中的指令
如果组队时有命令失败则全队失败,如果组队成功但是执行时有指令失败则没有错误的执行成功
discard
放弃组队
锁
乐观锁
watch
watch key
事务执行前使用,在事务执行时key发生改动,则事务被打断
悲观锁
秒杀
超时
使用Redis 连接池
超卖
使用乐观锁,事务使用前用watch
库存遗留
LUA脚本
将复杂的redis操作整合成一个LUA脚本,一次交给Redis执行,将能减少反复连接Redis的次数
LUA脚本类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作
Redis 2.6以上版本才能使用
分布式锁(Redis实现)
摘要
set KEY UUID nx ex 10
加锁
nx已经存在就不会重复插入
设置过期时间,避免服务器挂掉导致锁没释放
UUID避免删别人的锁
服务器抢到锁,但是卡住,时间过期时自动释放锁,然后b抢到锁,A恢复把b的锁释放了
del KEY
解锁
先判断UUID
LUA脚本确保原子性
避免判断UUID时到期锁自动释放,导致删错锁
为什么需要分布式锁?
单机锁用于限制一台机器多线程同时使用共享变量,而分布式锁是用来解决多台机器之间共享变量的使用(包括一台机器多个进程)
分布式锁可以使用MySQL、Redis、Zookeeper等实现,但是为了更好的性能,一般都使用Redis和Zookeeper
分布式锁怎么实现?
SET NX + 锁名称(SET if Not eXists)会在Redis里不存在时才能插入成功,插入成功后才能访问共享资源,访问完调用删除指令
如何避免死锁?
在加完锁后系统宕机,锁得不到释放,其他进程就无法获取锁
给锁增加超时时间
set KEY UUID nx ex 10
ex+超时时长
引入新问题
超时时间评估不准确,锁已经超时但还在执行业务逻辑
由于网络阻塞、系统GC等导致程序超过预定时间
可能释放其他客户端的锁
锁已经超时了,其他客户端拿到锁,这个时候前一个客户端以为自己还持有锁就把其他客户端的锁释放掉了
如何避免释放别人的锁
在执行指令SET key value时,key为锁名称,value为进程唯一标识。释放时利用唯一标识判断锁是不是自己持有
在释放锁时,校验是否是自己持有后释放锁要保证原子性
避免并发场景释放别人的锁
使用Lua脚本确保原子性
过期时间不好评估怎么办
时间设长一点,降低超时概率
利用守护线程检测过期时间,如果快超时自动续期
当发生主从切换时,分布式锁还安全吗
Redis作者提出一种解决方案Red Lock
Red Lock
如何实现
前提
不再需要哨兵和从库,只部署主库
主库部署多个实例,官方推荐至少5个
Red Lock步骤
1、客户端先获取时间戳T1
2、客户端依次向实例发送加锁请求,如果发生意外某个实例没加锁成功,则跳过这个实例,向下一个实例发送加锁请求
为什么要在多个实例上加锁?
为了容错,当部分机器发生故障时系统依然可用
3、请求完所有实例后,当加锁成功的实例超过半数,获取当前时间戳T2,如果T2-T1 < 锁过期时间,则认为加锁成功,反之加锁失败
为什么超过半数成功才算成功
保证正确性,如果小于半数也算成功,则可能同时有两个客户端拿到锁
为什么加锁成功后要计算耗时
因为可能因为网络等问题导致丢包,拿到最后一把锁时,可能第一把锁就过期了,导致没拿到半数锁单又以为拿到半数锁
4、加锁成功后去操作共享资源
5、加锁失败或者资源操作完释放锁,向所有实例(不能只发加锁成功的实例)发送释放锁的请求
为什么释放锁时要释放所有节点
因为可能网络问题,原先超时没拿到锁的节点其实是加锁成功的,如果不被释放,可能会影响下一次加锁
分布式专家 Martin 对于 Relock 的质疑
分布式锁的意义
效率
使用分布式锁的互斥,避免一些工作做两次,比如一些昂贵的计算任务,但是没锁住,发两次相同的邮件也无伤大雅
这种无关紧要的事情使用单机Redis就行,偶尔锁失效影响不大,red lock太重了
保证数据安全性
多个进程同时操作相同的数据会造成严重的数据错误、永久性不一致
red lock达不到安全性,还是会产生锁失效
锁在分布式系统会遇到的问题NPC
N
Network Dely网络延迟
P
Process Pause进程暂停
client1拿到锁后发生GC导致进程暂停,锁超时后,client2拿到锁开始操作共享资源,这个时候client1恢复后也开始操作共享资源,发生冲突
C
Clock Drift时钟漂移
假设时钟是正确的是不合理的
client1拿到ABC三台Redis的锁,C发生时钟漂移导致锁被释放,client2拿到CDE三台Redis的锁,此时二者都拿到半数以上的锁,可以操作共享资源
运维人员手工调整时钟、机器时钟在同步 NTP 时间时,发生了大的「跳跃」、机器宕机后立马重启都有这个问题
提出fencing token方案保证正确性
fencing token是递增的token,client1拿到token后发生GC,client2拿到token操作共享资源,client1恢复后操作共享资源被拒绝,因为他的token小于最新的token
red lock不能提供类似fencing token方案,所以解决不了正确性
作者反驳Martin
解释时钟问题
red lock并不需要完全一致的时钟,允许误差,只要不超过误差范围就行
手动修改时间的问题
不要去改就行
时钟跳跃
通过恰当运维将一次大的时钟跳跃改为多个小的时钟跳跃
关于GC进程暂停
Red Lock加锁步骤3时会校验锁的剩余时间,当校验T2-T1之前都是可以检测出锁过期
第3部之后其他锁也检测不出来,所以不讨论
质疑 fencing token 机制
这个方案必须要求要操作的「共享资源服务器」有拒绝「旧 token」的能力。
MySQL是可以通过乐观锁来达到拒绝旧Token的目的
UPDATE table T SET val = $new_val WHERE id = $id AND current_token < $token
但是像文件管理器这类就没有这种能力,如果有互斥能力就不需要分布式锁了
Red Lock虽然无法提供fencing token,但是可以达到一样的效果,Red Lock加完锁会像客户端返回UUID,可以使用这个UUID在MySQL上使用乐观锁
步骤
客户端使用 Redlock 拿到锁
客户端要修改 MySQL 表中的某一行数据之前,先把锁的 VALUE 更新到这一行的某个字段中(这里假设为 current_token 字段)
客户端处理业务逻辑
客户端修改 MySQL 的这一行数据,把 VALUE 当做 WHERE 条件,再修改UPDATE table T SET val = $new_val WHERE id = $id AND current_token = $redlock_value
基于Zookeeper分布式锁
过程
客户端1、2创建临时节点,比如/lock
客户端1创建成功,则1加锁成功,2加锁失败
客户端1操作共享资源
客户端1删除临时节点
优点
只要连接不断就不会释放锁,不用考虑过期时间
可以使用Watch等待锁释放
缺点
运维部署成本高
还是会有冲突的问题,例如拿到锁后GC,长时间没给Zookeeper发送心跳导致锁被释放,恢复后操作共享资源就导致冲突
性能不如Redis
数据结构
string
内部编码Encoding
int
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int。
embstr
如果字符串不是很长,保存为embstr,分配的SDS空间与redisObject相邻
raw
SDS空间和redisObject不相邻
SDS简单动态字符串
特征
SDS 不仅可以保存文本数据,还可以保存二进制数据。
SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
SDS 获取字符串长度的时间复杂度是 O(1)
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出
list
3.2前
如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
3.2后
List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
Hash
内部实现
如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
否则哈希表
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
Set
如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
Zset
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
缓存
缓存雪崩
请求进来先访问缓存再访问数据库的系统,当大量缓存在同一时间过期或者Redis故障时,请求直接打到数据库,严重会导致数据库宕机,从而导致整个系统崩溃
解决办法
俊宇设置过期时间
在设置过期时间时加上一个随机数
持久化
AOF Append Only File
redis.conf开启
先执行指令,再记aof日志
set name xiaolin
「*3」表示当前命令有三个部分
每部分都是以「$+数字」开头,后面紧跟着具体的命令、键或值,「数字」表示这部分中的命令、键或值一共有多少字节。
「$3 set」表示这部分有 3 个字节,也就是「set」命令这个字符串的长度。
Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的
优势
避免额外的检查开销
保证记录在 AOF 日志里的命令都是可执行并且正确的。
不会阻塞当前写操作命令的执行
缺点
服务器宕机,数据就会有丢失的风险
可能会给「下一个」命令带来阻塞风险。
AOP写回策略
redis写磁盘会导致服务性能下降,Redis提供3中写回磁盘的方案
Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
具体内核缓冲区的数据什么时候写入到硬盘,三种写回策略
redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填
Always
每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
Everysec
每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
No
不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机
AOF 重写机制
为什么要重写
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。Redis重启,用该aop文件恢复耗时会越来越多
重写机制会压缩aof文件大小
redis架构
简介
数据怕丢失
持久化(RDB/AOF)
恢复时间久
主从副本(副本随时可切)
故障手动切换慢
哨兵集群(自动切换)
读存在压力
扩容副本(读写分离)
写存在压力/容量瓶颈
分片集群
分片集群社区方案
Twemproxy、Codis(Redis 节点之间无通信,需要部署哨兵,可横向扩容)
分片集群官方方案
Redis Cluster (Redis 节点之间 Gossip 协议,无需部署哨兵,可横向扩容)
业务侧升级困难
Proxy + Redis Cluster(不侵入业务侧)
单机版 Redis
Redis的数据保存在内存,运行速度快,可以作为Mysql的缓存
但是如果Redis挂了,请求会直接打到MySQL,严重可能把MySQL打挂
因为内存在系统重启后会丢失,请求还是会打到MySQL,因此需要数据持久化,把数据写进磁盘
数据持久化
客户端的每次写操作,既需要写内存,又需要写磁盘,而写磁盘的耗时相比于写内存来说,肯定要慢很多!这势必会影响到 Redis 的性能。
内存数据写到磁盘,其实是分 2 步的
程序写文件的 PageCache(write)
把 PageCache 刷到磁盘(fsync)
程序写文件的 PageCache(write)
把 PageCache 刷到磁盘(fsync)
Redis 写内存由主线程来做,写内存完成后就给客户端返回结果,然后 Redis 用「另一个线程」去写磁盘,这样就可以避免主线程写磁盘对性能的影响。
主从复制:多副本
优势
缩短不可用时间:master 发生宕机,我们可以手动把 slave 提升为 master 继续提供服务
提升读性能:让 slave 分担一部分读请求,提升应用的整体性能
缺陷
当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。
哨兵:故障自动切换
要想自动切换,肯定不能依赖人了。
哨兵每间隔一段时间,询问 master 是否正常
master 正常回复,表示状态正常,回复超时表示异常
哨兵发现异常,发起主从切换
误判
如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会「误判
解决
一个哨兵会误判,那我们可以部署多个哨兵,让它们分布在不同的机器上,让它们一起监测 master 的状态
多个哨兵每间隔一段时间,询问 master 是否正常
master 正常回复,表示状态正常,回复超时表示异常
一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障
多个哨兵经过协商后,判定 master 故障,则发起主从切换
由哪个哨兵来发起主从切换呢?
选出一个哨兵「领导者」,由这个领导者进行主从切换。
怎么选
投票,选举规则
每个哨兵都询问其它哨兵,请求对方为自己投票
每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次
首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换
分片集群:横向扩展
什么是「分片集群」?
一个实例扛不住写压力,使用多个实例,利用路由规则将请求打到不同实例
每个节点各自存储一部分数据,所有节点数据之和才是全量数据
制定一个路由规则,类似哈希,相同的key一定在一个实例,客户端请求平均到各个实例
缺点
客户端需要维护这个路由规则,也就是说,你需要把路由规则写到你的业务代码中。
如何才能不把路由规则写入业务代码?
客户端和服务端之间增加一个「中间代理层」,这个代理就是我们经常听到的 Proxy,路由转发规则,放在这个 Proxy 层来维护。
Redis 在 3.0 其实就推出了「官方」的 Redis Cluster 分片方案,但由于推出初期不稳定,所以用的人很少,也因此业界涌现出了各种开源方案,上面讲到的 Twemproxy、Codis 分片方案就是在这种背景下诞生的。
Redis Cluster 方案的逐渐成熟,业界越来越多的公司开始采用官方方案
Redis Cluster 无需部署哨兵集群,集群内 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可发起自动切换。
关于路由转发规则,也不需要客户端自己编写了,Redis Cluster 提供了「配套」的 SDK,只要客户端升级 SDK,就可以和 Redis Cluster 集成,SDK 会帮你找到 key 对应的 Redis 节点进行读写,还能自动适配 Redis 节点的增加和删除,业务侧无感知。
虽然省去了哨兵集群的部署,维护成本降低了不少,但对于客户端升级 SDK,对于新业务应用来说,可能成本不高,但对于老业务来讲,「升级成本」还是比较高的,这对于切换官方 Redis Cluster 方案有不少阻力。
客户端无需做任何变更,只需把连接地址切到 Proxy 上即可,由 Proxy 负责转发数据,以及应对后面集群增删节点带来的路由变更。
0 条评论
下一页