目录
1 Redis简介
2 数据结构和对象
3 数据库
4 RDB
5 AOF
6 事件
7 客户端
8 复制
9 哨兵机制
10集群
11 发布与订阅
12 事务
13 缓存问题
14 内存淘汰机制
15 Redis并发竞争key
16 缓存与数据库一致性问题
17 Springboot整合Redis参考资料
· 《Redis设计与实现》
· JavaG
1 Redis 简介
简单来说redis就是一个数据库,不过与传统数据库不同的是redis的数据是存在内存中的,所以读写速度非常快,因此redis被广泛应用于缓存方向。另外,redis也经常用来做分布式锁。redis提供了多种数据类型来支持不同的业务场景。除此之外,redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。
2 数据结构和对象
redis数据库里面的每个键值对都是由对象组成的,其中,
· 数据库键总是一个字符串对象
· 而数据库键的值则可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
2.1 SDS简单动态字符串
2.1.1 SDS定义
保留空字符“\0”作为字符串的结尾,兼容C语言,但是空字符的1个字节不计入SDS的len属性中。
2.1.2 与C串的区别
(1)通过常数复杂度回去字符串长度,len属性
(2)字符串拼接、修改等操作时,杜绝缓存区溢出,自动修改大小。
(3)减少修改时内存重新分配次数
SDS通过未使用空间(free属性记录)解除了字符串长度和底层数组长度之间的关联。SDS实现了空间预分配和惰性空间释放两种优化策略。
· 空间预分配:优化SDS字符串增长操作。对象与修改len后,若len小于1MB,则free = len;若len大于等于1MB,则free = 1MB。故将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。
· 惰性空间释放:优化SDS字符串缩短操作。利用free属性将不适用的数据大小记录下来,等将来使用。
(4)二进制安全
所有的SDS API都会以处理二进制的方式来处理SDS存放的buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
2.2 链表
2.2.1 定义
2.2.2 特性
(1)双端
(2)无环
(3)带有表头和表尾指针
(4)带链表长度计数器
(5)多态,通过dup、free、match设置类型特定的函数
2.3 字典
2.3.1 定义
Redis的字典使用哈希表作为底层实现。
(1)哈希表节点
next指针将多个哈希值相同的键值对连接在一起,以此来解决键冲突问题(collision)。
(2)哈希表
(3)字典
ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
2.3.2 哈希算法
(1)计算hash值,采用MurmurHash2算法计算。
(2)计算在哈希表中的位置index = hash & sizemask
2.3.3 解决键冲突
(1)使用链地址法解决冲突
(2)因为dictEntry节点组成的链表没有表尾指针,故将新加节点加到链表的表头位置。
2.3.4 rehash重新散列
哈希表保存的键值对随着操作会逐渐增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,需要重新散列。
2.3.4.1 步骤
2.3.4.2 负载因子
(1)load_factor = ht[0].used / ht[0].size
(2)还要结合服务器是否在执行BGSAVE或者BGREWRITEAOF指令,在load_factor >= 1(否)或者load_factor >= 5(是)时进行rehash。
2.3.4.3 渐进式rehash
(1)rehash动作分多次、渐进式地完成.
(2)渐进式rehash期间,字典的增删查改在两个哈希表上进行。
2.4 skiplist跳跃表
是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
2.4.1 定义
(1)zskiplistNode
· 层用来加速访问其他节点,一般层的数量越多,访问其他节点的速度越快
· 层的前进指针:用于从表头向表尾访问节点
· 跨度:用来计算排位的,查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在条鱼表中的排位。
· 后退指针:用于从表尾向表头方向访问节点,每次只能后退至前一个节点。
· 分值:跳跃表中的所以节点按照分值从小到大排序
· 成员对象:指向一个字符串对象SDS,且对象必须唯一
(2)zskiplist
typedef struct zskiplist {
// 表头节点和表尾节点
structz skiplistNode *header, *tail;// 表中节点的数量
unsigned long length;// 表中层数最大的节点的层数
int level;} zskiplist;
2.4.2 应用
(1)有序集合键
(2)集群节点中用作内部数据结构
2.5 intset整数集合
2.5.1 定义
· encoding属性决定contents数组真正的类型可以为INTSET_ENC_INT16(int16_t),INTSET_ENC_INT32(int32_t),INTSET_ENC_INT64(int64_t)
· length属性是contents数组的长度
· contents数组是整数集合的底层实现,各个按值大小从小到大有序地排列,不包含重复项。
2.5.2 升级
当新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级。
(1)步骤
· 根据新元素的类型,扩展整数集合底层数组的空间大小
· 底层数组现有的所有元素都转换成与新元素相同的类型,并保持数组的有序性。
· 将新元素添加到底层数组里面
(2)新元素插入的位置
要么是最大值要么是最小值,故只会在表头和表尾插入。
(3)优点
· 提升整数集合的灵活性
· 尽可能地节约内存
2.6 ziplist压缩列表
2.6.1 定义
(1)ziplist
(2)节点entry
· previous_entry_length:记录了压缩列表中前一个节点的长度。压缩列表从表尾向表头遍历操作就是依赖这个属性。
· encoding:记录节点的content属性所保存数据的类型和长度。
· content 保存节点的值,可以是一个字节数组或者一个整数。
2.6.2 连锁更新
当添加或者删除节点的时候,导致previous_entry_length所占字节空间发生变化(1字节或者5字节),新节点的后续节点都要重新分配空间。
2.7 对象
包含5中类型的对象。基于引用计数计数进行内存回收和对象共享机制。
2.7.1 redisObject
(1)结构定义
(2)type
(3)encoding
2.7.2 字符串对象
(1)可以使用的encoding类型
REDIS_ENCODING_INT、REDIS_ENCODING_EMBSTR、REDIS_ENCODING_RAW
(2)编码转换
int编码和embstr编码的字符串对象在条件满足的情况下,被转换为raw对象。
2.7.3 列表对象
(1)可以使用的encoding类型
REDIS_ENCODING_ZIPLIST、REDIS_ENCODING_LINKEDLIST
(2)编码转换
2.7.4 哈希对象
(1)可以使用的encoding类型
REDIS_ENCODING_ZIPLIST、REDIS_ENCODING_HT
(2)编码转换
2.7.5 集合对象
(1)可以使用的encoding类型
REDIS_ENCODING_INTSET、REDIS_ENCODING_HT
(2)编码转换
2.7.6 有序集合对象
(1)可以使用的encoding类型
REDIS_ENCODING_ZIPLIST、REDIS_ENCODING_SKIPLIST
(2)底层结构zset
typedef struct zset{
zskiplist *zsl;
dict *dict;
}
· zsl按分值从小到大保存所有集合元素
· dict保存成员到分值的映射,键为成员,值为分值。
这两种数据结构通过指针来共享相同元素的成员和分值,不会浪费额外的内存。
(3)编码转换
2.7.7 多态命令
redis除了会根据值对象的类型来判断键是否能够执行指定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。
2.7.8 内存回收
引用计数法:利用redisObject中的refcount属性,当计数值为0时,回收内存。
2.7.9 对象共享
· 将数据库键的值指针指向一个向右的值对象
· 将被共享的值对象的引用计数增1
· 注意:redis只对包含整数值的字符串对象进行共享,其他字符串可能比较值相同较耗CPU。
2.7.10 空转时长
· redis利用redisObject中的lru属性,记录对象最后一次被命令程序访问的时间。
· 空转时长,就是通过将当前时间减去键的值对象的lru时间计算得出
· 如服务器打开maxmemory选项,且回收内存算法为volatile-lru或者allkeys-lru,则当内存超过maxmemory时,空转时长较高的部分将优先被服务器释放,回收内存。
3 数据库
3.1 定义
(1)redisServer
struct redisServer{
// 保存服务器中所有数据库的数组
redisDb *db;// 服务器数据库数量
int dbnum;// ....
}
dbnum的数量默认为16.
(2)redisClient
typedef struct redisClient{
// 记录客户端当前正在使用的数据库
redisDb *db;// ....
}
默认使用0号数据库。可以通过SELECT命令切换数据库。
(3)redisDb
typedef struct redisDb{
// 数据库键空间
dict *dict// 过期时间
dict *expires;// ...
}
· dict字典中保存一个数据库中的所有键值对。
· 脏键,客户端使用WATCH命令监视的键,服务器每次修改一个键之后,都会对脏键计数器的值加1,计数器会触发服务器的持久化已经复制操作。
3.2 过期时间
(1)EXPIRE/PEXPIRE命令
设置TTL生存时间,EXPIRE单位为s,PEXPIRE单位为ms。
(2)EXPIREAT/PEXPIREAT命令
设置过期时间,后面跟UNIX时间戳。
(3)使用 redisDb的expires字典保存过期时间,键为对象,值为过期时间。
(4)redis的过期删除策略
· 惰性删除:在查询key时,若过期才删除
· 定期删除:当服务器周期操作serverCron函数执行时删除。每次删除随机抽取部分,并维护一个进度记录,知道过期键全部清除。
(5)RDB功能和过期键
· 生成RDB文件时,检测键,过期的键不会被保存到文件中。
· 载入RDB文件时,主服务器忽略过期键,从服务器直接载入(主从同步时会删除)。
(6)AOF功能和过期键
过期键对AOF无影响,当过期键被删除式,AOF文件追加DEL语句。
4 RDB
4.1 概念
(1)RDB持久化是将某个时间点上的数据库状态保存到一个RDB文件中。
(2)RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。
4.2 创建和载入
4.2.1 创建
(1)SAVE命令
阻塞Redis服务器,直到RDB文件创建完毕为止。
(2)BGSAVE命令
派生出一个子进程,子进程负责创建RDB,服务器进程继续处理命令。
· 注意:如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据。当AOF关闭时,才会使用RDB方式。
4.2.2 载入
在服务器启动时自动执行。
4.3 自动间隔性保存
(1)配置
在redis.windows.conf中,通过save选项设定服务器自动执行BGSAVE命令的间隔时间。
· 900秒内,对数据库修改1次,创建RDB
· 300秒内,对数据库修改10次,创建RDB
· 60秒内,对数据库修改10000次,创建RDB
(2)原理
· save选项会设置到redisServer的saveparam属性中。
· redisServer的dirty计数器记录一次SAVE/BGSAVE后,数据库修改次数
· redisServer的lastsave属性记录上一次SAVE/BGSAVE执行时间
· 服务器周期性操作函数serverCron默认每个100ms执行一次,其中一项工作为检测save选项条件是否满足
4.4 RDB文件结构
(1)REDIS用来快速检测所载入的文件是否是RDB文件
(2)db_version长度为4字节,记录RDB文件版本号
(3)databases包含0个或多个数据库,以及各数据库中的键值对数据
(4)EOF长度为1字节,标志RDB文件正文内容的结束
(5)check_sum长度为8字节无符号整数,保存一个校验和,用来检测RDB文件是否有出错或者损坏的情况
5 AOF
5.1 概念
AOF持久化是通过保存Redis服务器锁执行的写命令来记录数据库状态的
5.2 创建步骤
5.2.1 命令追加(append)
服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到redisServer的aof_buf缓冲区(SDS类型)的末尾。
5.2.2 文件写入与同步
服务器在每个事件处理结束时,调用flushAppendOnlyFile函数将aof_buf缓冲区中的内容写入和保存到AOF文件里面。flushAppendOnlyFile函数的appendfsync参数决定同步(何时将操作系统内存缓存区的内容写入磁盘)方式:
everysec为默认设置。
5.3 载入
步骤:
(1)创建一个不带网络连接的伪客户端
(2)从AOF文件中分析并读取一条写命令(包括新增,修改,删除)
(3)使用伪客户端执行写命令
(4)一直执行(2)和(3)直到所有命令被处理完。
5.4 AOF重写
(1)原因:为了解决长时间后AOF文件体积膨胀的问题(主要由于Redis的内存淘汰机制,一定时间后,大量数据被淘汰,使得原本的AOF存在大量之前的写记录,变得冗长)
(2)实现:创建一个新的AOF文件代替现有AOF文件。新旧两个AOF文件保存的数据库状态相同,但新AOF文件不包含任何浪费空间的冗命令。
(3)原理:从数据库读取键现在的值,用一条命令去记录键值对,代替之前记录这个键值对的多条命令。
(4)后台重写:创建子进程执行AOF重写程序(因为Reids采用单线程模式工作)。
6 事件
Redis服务器是一个事件驱动程序
6.1 文件事件
6.1.1 构成
(1)套接字 socket
Redis服务器通过套接字与客户端连接。
(2)I/O多路复用
将所有产生的套接字都放在一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。
Redis以单线程模式运行。
(3)文件事件分派器
根据IO复用器传过来的套接字产生的事件类型,调用相应的事件处理器
(4)事件处理器
· 命令请求处理器
· 命令回复处理器
· 连接应答处理器
· 。。。
6.1.2 事件类型
(1)AE_READABLE
套接字可读。即客户端对套接字执行write操作,或者close操作,或者有新的可应答套接字出现。
(2)AE_WRITABLE
套接字可写,即客户端对套接字执行read操作。
当两种事件同时发生时,文件事件分派器优先处理AR_READABLE事件。
6.2 时间事件
分为定时事件和周期性事件两类。
6.2.1 分类
(1)定时事件:让一段程序在指定的时间后执行一次。事件处理器返回AE_NOMORE
(2)周期性事件:让程序每隔指定时间就执行一次。事件处理器返回非AE_NOMORE整数值。
目前版本Redis只使用周期性事件。
6.2.2 时间事件的属性
· id 服务器为时间事件创建的全局唯一ID
· when 毫秒精度的UNIX时间戳,记录时间事件的到达时间
· timeProc 时间时间处理器,当时间事件到达时,服务器会调用相应的处理器来处理事件
6.2.3 实现
服务器将所有时间事件放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
· 新的时间事件总是插入到链表的表头。
· 正常模式下的Redis服务器只使用serverCron一个时间事件,故无序链表并不影响事件执行性能。
6.2.4 serverCron函数
(1)Redis需要定期对自身的资源和状态进行检查和调整
(2)主要工作:
· 更新服务器的各类统计信息,如时间、内存占用等
· 清理数据库中的过期键值对
· 关闭和清理连接失效的客户端
· 尝试进行AOF或RDB持久化操作
· 如果服务器是主服务器,对从服务器进行定期同步
· 如果处于集群模式,对集群进行定期同步和连接测试
(3)默认serverCron平均每间隔100ms运行一次(Redis2.6),Redis2.8开始,用户通过修改redis.windos.conf的hz选项来调整每秒执行次数。
6.3 事件的执行原则
(1)一次文件事件之后,仍然没有时间事件到达,那么服务器将再次等待并处理文件事件。
(2)事件的处理都是同步、有序、原子地执行的。
(3)时间事件在文件事件之后执行,通常执行时间会比时间事件设定的到达时间稍晚一些。
7 客户端
7.1 redisClient
struct redisServer {
// ...
// 一个链表,保存了所有客户端状态
list *client// 套接字描述符
int fd;//...
}
7.2 客户端分类
(1)普通客户端
· fd > -1的整数,来源于网络
· 创建时,添加到链表表尾
(2)伪客户端
· fd = -1,不是来源于网络
· AOF在载入AOF文件时创建。在载入完成后,伪客户端关闭。
· Lua脚本执行时创建。在服务器关闭时,伪客户端关闭。
8 复制
8.1 概念
(1)"SLAVEOF ip port"命令或者配置文件中的slaveof选项
(2)主从服务器的数据库将保存相同的数据
8.2 旧版(Redis2.8之前)
分为两个步骤进行,同步和命令传播。
8.2.1 同步
(1)从服务器向主服务器发送SYNC命令
(2)收到SYNC命令的主服务器执行BGSAVE命令。在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
(3)主服务器将RDB文件发送给从服务器,从服务器收入并载入RDB文件。
(4)主服务器将记录在缓冲区里面的所有写命令发送给从服务器。
8.2.2 命令传播
同步之后,每次将主服务器的写命令发送给从服务器。
8.3 新版
为了解决旧版复制功能在处理断线重复制情况时的低效问题。
8.3.1 实现
使用PSYNC命令,具有完整重同步和部分重同步两种模式
(1)完整重同步,用于初次复制情况
和SYNC命令类似,传送RDB文件和写命令缓冲区。
(2)部分重同步,用于处理断线后重复制情况
主服务器将主从服务器连接断开期间执行的写命令发送给从服务器。
8.4 心跳检测
(1)在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:
REPLCONF ACK <replication_offset>
(2)作用:
· 检测主从服务器的网络连接状态
· 辅助实现min-slaves选项
· 检测命令丢失
9 哨兵机制
9.1 意义
sentinel是Redis的高可用性解决方案。监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。
若之前的主服务器重新上线,则自动成为现存主服务器的从服务器。
9.2 启动sentinel
(1)初始化服务器
· Sentinel本质上是一个运行在特殊模式下的Redis服务器
· Sentinel不适用数据库,不会载入RDB或者AOF文件
(2)将普通Redis服务器使用的代码替换为sentinel专用代码
(3)初始化sentinel状态
(4)根据配置文件,初始化Sentinel的监视主服务器列表
(5)创建连向主服务器的网络连接
· 命令连接:向服务器发送命令和接收命令回复
· 订阅连接:订阅服务器的_sentinel_:hello频道
9.3 获取服务器信息
(1)Sentinel默认以十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令。
可以获取以下消息:
· 主服务器本身的信息
· 所有从服务器的信息
(2)Sentinel默认以十秒一次的频率,通过命令连接向被监视的从服务器发送INFO命令。
9.4 发送和接收
(1)sentinel每两秒一次发送命令给监视的主从服务器的_sentinel_:hello频道
(2)订阅连接建立之后,通过_sentinel_hello频道获取信息。
9.5 检测是否下线
(1)主观下线
Sentinel默认每次一秒的频率向建立了命令连接的Redis实例发送PING命令。若在down-after-milliseconds选项配置的时间内没有有效回复,认为为主观下线状态。
(2)客观下线
当Sentinel将一个主服务器判断为主观下线后,为了确认是否真的下线了,会向同样监视这一主服务器的其他Sentinel进行询问,若其他Sentinel也认为为下线状态,在接收到足够数量的下线判断后,Sentinel认为主服务器为客观下线,并进行故障转移操作。
9.6 选举领头Sentinel
当一个主服务器为客观下线时,监视这个主服务器的所有Sentinel选举一个领头Sentinel对主服务器执行故障转移操作。
主要步骤如下:
(1)在一个配置纪元(计数器)里面,所有Sentinel有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。
(2)Sentinel向另一个Sentinel发送带有自己运行ID的命令,让其设置自己为局部领头Sentinel(相当于抢票)。
(3)局部领头Sentinel规则:先到先得。已经设置为别人为Sentinel的Sentinel,拒绝后续收到的设置
(4)如果某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。
9.7 故障转移
(1)在已下线主服务器属下的所有从服务器中,挑选出一个从服务器,将其转换为主服务器。
领头Sentinel按照从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器
(2)让已下线主服务器属下的所以从服务器改为复制新的主服务器。
(3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。
10集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片进行数据共享,并提供复制和故障转移功能。
10.1 节点
(1)CLUSTER MEET命令
· 格式 CLUSTER MEET <ip> <port>
· 向另一个节点发送命令,进行握手,握手成功后加入所在集群。
(2)启动节点
(3)集群数据结构
· clusterNode 每个节点使用其记录自己的状态,并为集群中其他节点创建一个相应的clusterNode结构
· clusterLink 保存了连接节点所需的有关信息
· clusterState 记录在当前节点的视角下,集群目前所处状态
(4)节点握手
10.2 槽指派
(1)概念
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384割槽(slot),数据库中的每个键都属于整个16384个槽的其中一个,集群中的每个节点可以处理0~16384个槽。
(2)数据结构
struct clusterNode{
//....
unsigned char slots[16384/8];
int numslots;
// ....
}
· 二进制数组中索引上的二进制位为1,则表示节点负责处理该槽
· numslots表示该节点负责的槽数量
(3)相关命令
CLUSTER ADDSLOTS 指派槽
(4)注意点
节点数据库只能使用0号数据库,这和单机服务器的数据库不同。
10.3 复制和故障转移
10.3.1 复制
主节点(master)用于处理槽,从节点用于复制某个主节点,在其主节点下线时可以代替主节点继续处理命令请求。
10.3.2 故障转移
(1)在从节点中选一个成为新的主节点
新主节点的选取,类似于sentinel领头的选取,算法都是基于Raft算法实现的。
(2)新的主节点撤销对已下线主节点的槽指派,并将这些槽指派给自己
(3)新的主节点广播PONG消息,让其他节点知道自己成为新的主节点
(4)接收和处理自己的槽相关的请求,故障转移完成。
11 发布与订阅
11.1 概述
(1)由PUBLISH(发送消息)、SUBSCRIBE(订阅频道)、PSUBSCRIBE(订阅模式)等命令组成
(2)通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,每当有其他客户端向被订阅的频道发送消息时,频道的所有订阅者都会受到这条消息。
(3)通过执行PSUBSCRIBE命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息会被发送给与这个频道相匹配的模式的订阅者。
11.2 频道
11.2.1 数据结构
struct redisServer{
// ...
// 保存所有频道的订阅关系
dict *pubsub_channels;// ...
}
· 字典的键是某个被订阅的频道
· 字典的值是一个链表,记录所有订阅该频道的客户端
11.2.2 订阅和退订
(1)订阅:使用SUBSCRIBE命令,在链表的尾部添加
(2)退订:使用UNSUBSCRIBE命令,从链表中删除客户端。当出现键对应空链表,要从字典中删除键
11.3 模式
11.3.1 数据结构
struct redisServer{
// ...
// 保存所有频道的订阅关系
dict *pubsub_patterns;// ...
}
11.3.2 订阅和退订
类似频道。
11.4 发送消息
PUBLISH命令:在pubsub_channels字典里找到频道channel的订阅者名单(一个链表),然后将消息发送给名单上的所有客户端。
12 事务
12.1 概述
使用MULTI(事务开始)、EXEC(提交事务)、WATCH等命令来实现事务。
事务提供了一种多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求。
12.2 事务的实现
(1)事务开始
· MULTI命令将客户端切换至事务状态。
· 原理:在客户端状态的flags属性中打开REDIS_MULTI标识。
(2)命令入队
· 如果是EXEC、DISCARD、WATCH、MULTI四个命令,立即执行
· 其他命令放入事务队列,然后客户端返回QUEUED回复
(3)事务执行
遍历客户端的事务队列,执行队列中保存的所有命令。
12.3 WATCH命令
(1)作用
是一个乐观锁,在执行EXEC命令前,监视任意数量的数据库键。在EXEC命令执行时,若监视的键是否至少有一个已经被修改过了,如果是的话,服务器拒绝执行事务,向客户端返回空回复。
(2)数据结构
typedef struct redisDb{
// ...
// 正在被WATCH命令监视的键
dict *watched_keys;// ...
}
每个数据库都保存一个字典,键为WATCH命令监视的数据库键。值为一个链表,记录所有监视相应数据库键的客户端。
(3)当被监视的键被修改,则客户端的REDIS_DITRY_CAS标识打开。
13 缓存问题
13.1 缓存雪崩
(1)原因
缓存同一时间大面积的失效,大量的请求直接落到数据库上,造成数据库短时间内承受大量请求而崩掉。
(2)解决办法
· 事前:尽量保证整个Redis集群的高可用性,发现机器宕机几块补上,选择合适的内存淘汰策略。
· 事中: 本地ehcache缓存+hystrix限流&降级,避免MySQL崩掉
· 事后:利用redis持久化机制保存的数据尽快恢复缓存
13.2 缓存穿透
13.2.1 原因
大量请求的key根本不在缓存中,导致请求直接到了数据库上。
一般MySQL的最大连接数在150左右,最大连接数还只是一个指标,cpu,内存,自盘,网络等
13.2.2 解决办法
13.2.2.1 无效key的时间减短
黑客每次构建不同的请求Key,会导致redis中缓存大量无效的key,故可以将无效key的过期时间设置短一点。
13.2.2.2 布隆过滤器
(1)布隆过滤器的概念
布隆过滤器(Bloom Filter)可以看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。
总结:一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。
(2)布隆过滤器的原理介绍
· 当一个元素加入布隆过滤器中的时候,会进行如下操作:
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)
根据得到的哈希值,在位数组中把对应下标的值置为 1
· 当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:
对给定元素再次进行相同的哈希计算
得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中
举个简单的例子:
不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。
综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
在Redis中具体工作机制如下:
14 内存淘汰机制
保证Redis中的数据为热点数据。
(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
(3)volatile-random:从已设置过期时间的数据集中挑选任意数据淘汰
(4)allkeys-lru:内存不足时,在键空间中移除最近最少使用的key(最常用)
(5)allkeys-random:从数据集中任意选择数据淘汰
(6)no-eviction:禁止驱逐数据
redis4.0后新增
(7)volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
(8)allkeys-lfu:内存不足时,在键空间中移除最不经常使用的key
15 Redis并发竞争key
分布式锁一般有三种方式实现:数据库乐观锁,基于Redis的分布式锁,基于zookeeper的分布式锁。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
这个set()方法一共有五个形参:
· 第一个为key,我们使用key来当锁,因为key是唯一的。
· 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
· 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
· 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
· 第五个为time,与第四个参数相呼应,代表key的过期时间。
方法底层主要使用Redis的Setnx 命令实现。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
16 缓存与数据库一致性问题
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
· 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
· 更新的时候,先删除缓存,然后再更新数据库。
17 Springboot整合Redis
17.1 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
17.2 配置文件
spring:
#redis
redis:
host: localhost
password: 123456
port: 6379
pool:
max-idle: 100
min-idle: 1
max-active: 1000
max-wait: -1
17.3 Service层代码
@Service
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;public void setStr(String key, String value) {
setStr(key, value, null);
}
public void setStr(String key, String value, Long time) {
stringRedisTemplate.opsForValue().set(key, value);
if (time != null)
stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}
public Object getKey(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public void delKey(String key) {
stringRedisTemplate.delete(key);
}
}