详解Redis
核心知识点
基本数据类型
基本的5种
- String(字符串),使用SDS,即Redis自己构建的简单动态字符串来实现;
- List(列表),用的是双向链表,支持反向查找;
- Hash(散列),键的值是键值对,可以用于存储对象,实现类似Java的HashMap;
- Set(集合),无序结构且无重复,支持交并差集的运算,可用于共同关注、粉丝等功能
- Zset(有序集合),跳表实现,针对Set中每个元素增加一个权重参数score,根据这个参数有序排列;可以根据score范围获取元素列表,可以用于实现排行榜
特殊的3种
- HyperLogLog(基数统计)
- Bitmap (位图)⚠️
- Geospatial (地理位置)
底层8种类型
- 简单动态字符串(SDS)
- LinkedList(双向链表)
- Dict(哈希表/字典)
- SkipList(跳跃表):用于Sorted Set
- Intset(整数集合):紧凑地存储多个整数⚠️
- ZipList(压缩列表)⚠️
- QuickList(快速列表)⚠️
缓存读写策略
旁路缓存模式
Cache Aside Pattern,最经常使用的一种
读:读缓存,读不到则读db,读完放缓存
写:写db,然后直接删了cache
- 这里先写db还是先删cache,其实都可能导致db和cache不一致,但删cache速度快,所以不一致的概率低
- 适合读比较多,如果写的多,那cache会一直被删,非常麻烦
读写穿透模式
Read/Write Through Pattern,相当于Redis端有一个守护进程,把底层数据库屏蔽掉了
读:读缓存,读不到则把数据从db加载到cache,再读cache
写:先查cache,如果没有则直接更新db;如果有则更新cache,然后由cache服务自己更新db
异步缓存写入
Write Behind Pattern,不常用,与读写穿透类似,但是写的过程是异步的
- 读法和读写穿透差不多
- 写法和读写穿透的差异在于,更新db是异步实现的,例如mysql中的redo log也是异步刷盘
持久化机制
支持3种持久化机制使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)
RDB(快照, snapshotting)
- (干嘛的)通过创建快照来获得存储在内存里面的数据在某个时间点上的副本
- (有什么用)将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构)
- (怎么用)可以阻塞主线程,也可以fork出子进程执行
AOF(只追加文件, append-only file)
大致流程:
- 先执行完命令,再写到AOF缓冲区,(这样就不用语法检查了)
- 系统调用
write
函数写到系统内核缓冲区, - 使用
fsync
系统调用,刷新系统内核缓冲区到磁盘中。
持久化方式(fsync时机)
可选项:fsync
刷磁盘的时机可以选择:可以write后直接fsync,也可以每秒一次fsync,也可以让os自行决定
AOF重写
当AOF文件太大时,进行优化缩小,会单独开一个子进程
重写期间会维护一个AOF重写缓冲区,在创建新的AOF期间,记录服务器执行的所有命令,然后重写完后把这些命令都追加到新的AOF末尾。
混合持久化
RDB和AOF一般是一起开启的。
- RDB 是压缩二进制数据,文件很小,恢复速度快;但是生成的过程比较繁重;
- AOF文件比较大,但是写入快,数据安全性高(支持秒级数据丢失,取决于fsync策略)
事务
把命令放进一个队列中依次执行
可能执行失败,后续命令仍然执行
部署方式
主从复制 => 哨兵模式
- 主从复制:
优点:1. 主节点负责写,从节点负责读,读写分离提高性能;2. 数据存在备份,有一定安全性
缺点:1. 单点故障风向高,没有高可用性;2. 数据同步延迟,一致性难保证;3. 手动转移故障,耗时复杂
- 引入哨兵:
监控redis集群各个节点是否正常
通知其他节点某个节点出现故障
自动故障转移,把一个从节点升级为主节点,并将其他节点的主节点设置好
哨兵可以有多个(一般3个),用选举方式(选举算法raft)选举出领导者来监控哨兵,哨兵的部署也得是高可用的
1 | // 配置文件 sentinel.conf |
1 | // 启动配置文件 |
在切换的时候对外是不提供服务的,但也差不多挺高可用了
集群模式 Cluster
这个解决了前面的架构数据量太大的问题
如何添加新节点?和一台机器进行3次握手
数据公平性
一共16384个槽位,每个节点(节点是主从复制架构)负责一部份;
数据读写的时候,对key进行哈希运算,映射到哪就哪个小集群负责;
对于每个小集群,每次数据来的时候,检查数据是不是自己负责,不是的话就需要把正确的槽号、IP和端口返回回去,让正确的小集群处理
Redisson
Redis有个命令叫做SETNX key value
,只有在 key 不存在时设置 key 的值(set if not exist),这个name就是锁的标识符,如果两个进程同时要抢某个锁,则那个成功set的抢到了锁
问题:如果有了锁之后服务器宕机了,那锁永远都无法被解除;如果设置锁的过期时间,则可能业务没执行完就释放锁了
解决方案是Redisson分布式锁:
key是锁的名称,value是个Hash,键是进程id,值是重入次数;如果相同进程pid又来获取锁,则会重入次数+1;
Watch Dog机制,看门狗:只要占用锁的进程还没挂掉、且没有释放锁,都会每隔10秒进行续约30秒(看门狗能看到进程的pid,通过判断)
加锁失败(key已经存在):进入循环,被Semaphor阻塞住(AQS);当锁被释放后,通过Semaphor唤醒,并再次执行加锁逻辑,成功后进程则被跳出循环;
释放锁:看这个锁的进程id的键是不是自己,如果不是则没有权限解除,是的话就把重入次数-1,如果减完不等于0,则更新锁的过期时间;如果减完=0,则释放锁,并发布释放锁的消息,唤醒被阻塞的进程。
- Redisson获取锁的代码流程解读:
通过tryLock函数,携带waitTime和leaseTime,waitTime是等待时间,即没抢到锁的时候等待多久,leaseTime是存活时间(即锁多久自动释放)
tryLock中使用lua脚本进行redisson抢锁操作,lua来保证redis操作的原子性;具体操作就是判断键是否存在于redis中
如果键不存在,则创建以锁的name为key的value,其value是redis的hash结构,hash中加入一个当前进程pid的键和重入次数1的值;
如果键存在,则要判断键的hash中是否存在当前进程pid的键,如果存在则重入次数+1;不存在则进入循环等待
如果循环等待的时间超过waitTime则返回失败
进程会创建一个线程,每隔1/3的leaseTime去看一下,如果还没有执行完毕,则续约到最初始的时间
- 尝试获取锁的代码:
- 续约的代码: