Redis
2025-09-04 22:33:40 0 举报
AI智能生成
Redis学习总结
作者其他创作
大纲/内容
基础数据结构
String
常用操作
SET key value //存入字符串键值对
MSET key value [key value ...] //批量存储字符串键值对
SETNX key value //存入一个不存在的字符串键值对
GET key //获取一个字符串键值
MGET key [key ...] //批量获取字符串键值
DEL key [key ...] //删除一个键
EXPIRE key seconds //设置一个键的过期时间(秒)
原子加减
INCR key //将key中储存的数字值加1
DECR key //将key中储存的数字值减1
INCRBY key increment //将key所储存的值加上increment
DECRBY key decrement //将key所储存的值减去decrement
分布式锁
SETNX product:10001 true //返回1代表获取锁成功
SETNX product:10001 true //返回0代表获取锁失败
。。。执行业务操作
DEL product:10001 //执行完业务释放锁
SET product:10001 true ex 10 nx //防止程序意外终止导致死锁
Hash
常用操作
HSET key field value //存储一个哈希表key的键值
HSETNX key field value //存储一个不存在的哈希表key的键值
HMSET key field value [field value ...] //在一个哈希表key中存储多个键值对
HGET key field //获取哈希表key对应的field键值
HMGET key field [field ...] //批量获取哈希表key中多个field键值
HDEL key field [field ...] //删除哈希表key中的field键值
HLEN key //返回哈希表key中field的数量
HGETALL key //返回哈希表key中所有的键值
HINCRBY key field increment //为哈希表key中field键的值加上增量increment
应用场景
• 电商购物车
1)以用户id为key
2)商品id为field
3)商品数量为value
• 购物车操作
1) 添加商品 hset cart:1001 10088 1
2) 增加数量 hincrby cart:1001 10088 1
3) 商品总数 hlen cart:1001
4) 删除商品 hdel cart:1001 10088
5) 获取购物车所有商品 hgetall cart:1001
优缺点
• 优点
1)同类数据归类整合储存,方便数据管理
2)相比string操作消耗内存与cpu更小
3)相比string储存更节省空间
• 缺点
1) 过期功能不能使用在field上,只能用在key上
2) Redis集群架构下不适合大规模使用
1)同类数据归类整合储存,方便数据管理
2)相比string操作消耗内存与cpu更小
3)相比string储存更节省空间
• 缺点
1) 过期功能不能使用在field上,只能用在key上
2) Redis集群架构下不适合大规模使用
List
常用操作
LPUSH key value [value ...] //将一个或多个值value插入到key列表的表头(最左边)
RPUSH key value [value ...] //将一个或多个值value插入到key列表的表尾(最右边)
LPOP key //移除并返回key列表的头元素
RPOP key //移除并返回key列表的尾元素
LRANGE key start stop //返回列表key中指定区间内的元素,区间以偏移量start和stop指定
BLPOP key [key ...] timeout //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
常用数据结构
Stack(栈) = LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP
常见应用场景
视频列表、签到列表、排队机、简化版的MQ
注意点
1)一个list的容量是2的32次方减1个元素,大概40多亿。但是在应用时,要注意大key的问题。
2)list的底层是一个双向链表,对双端的操作性能很高。但是通过索引下表直接操作某一个中间节点的性能就会比较低。
Set
常用操作
SADD key member [member ...] //往集合key中存入元素,元素存在则忽略,若key不存在则新建
SREM key member [member ...] //从集合key中删除元素
SMEMBERS key //获取集合key中所有元素
SCARD key //获取集合key的元素个数
SISMEMBER key member //判断member元素是否存在于集合key中
SRANDMEMBER key [count] //从集合key中选出count个元素,元素不从key中删除
SPOP key [count] //从集合key中选出count个元素,元素从key中删除
运算操作
SINTER key [key ...] //交集运算
SINTERSTORE destination key [key ..] //将交集结果存入新集合destination中
SUNION key [key ..] //并集运算
SUNIONSTORE destination key [key ...] //将并集结果存入新集合destination中
SDIFF key [key ...] //差集运算
SDIFFSTORE destination key [key ...] //将差集结果存入新集合destination中
应用场景
1)点击参与抽奖加入集合
SADD key {userlD}
2)查看参与抽奖所有用户
SMEMBERS key
3)抽取count名中奖者
SRANDMEMBER key [count] / SPOP key [count]
1) 点赞
SADD like:{消息ID} {用户ID}
2) 取消点赞
SREM like:{消息ID} {用户ID}
3) 检查用户是否点过赞
SISMEMBER like:{消息ID} {用户ID}
4) 获取点赞的用户列表
SMEMBERS like:{消息ID}
5) 获取点赞用户数
SCARD like:{消息ID}
SINTER set1 set2 set3 { c } 共同关注的人
SUNION set1 set2 set3 { a,b,c,d,e } 朋友圈的人
SDIFF set1 set2 set3 { a } 推荐好友
ZSet
常用操作
ZADD key score member [[score member]…] //往有序集合key中加入带分值元素
ZREM key member [member …] //从有序集合key中删除元素
ZSCORE key member //返回有序集合key中元素member的分值
ZINCRBY key increment member //为有序集合key中元素member的分值加上increment
ZCARD key //返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES] //正序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES] //倒序获取有序集合key从start下标到stop下标的元素
集合操作
ZUNIONSTORE destkey numkeys key [key ...] //并集计算
ZINTERSTORE destkey numkeys key [key …] //交集计算
应用场景
1)点击新闻
ZINCRBY hotNews:20190819 1 守护香港
2)展示当日排行前十
ZREVRANGE hotNews:20190819 0 9 WITHSCORES
3)七日搜索榜单计算
ZUNIONSTORE hotNews:20190813-20190819 7
hotNews:20190813 hotNews:20190814... hotNews:20190819
4)展示七日排行前十
ZREVRANGE hotNews:20190813-20190819 0 9 WITHSCORES
Bitmap
常用操作
SETBIT key offset value //将一个二进制数组的offset位置设置成value。value只能是0或者1。
GETBIT key offset //返回一个二进制数组的offset位置的值。
BITCOUNT key [start end [BYTE|BIT]] //返回二进制数组中1的个数
BITPOS key bit [start [end [BYTE|BIT]]] //返回bitmap中第一个值为bit的offset位置。
BITOP AND|OR|XOR|NOT destkey key [key ...] //对两个bitmap做二进制的与或非计算。
应用场景
SETBIT dailycheck:1 100 1 1号用户第100天完成了签到
BITCOUNT dailycheck:1 统计1号用户的签到次数
BITPOS dailycheck:1 统计1号用户第一天签到的时间
优点
快速、高效、节省空间
Hyperloglog
用于统计一个集合中不重复的元素个数。典型应用场景例如根据用户访问记录统计网站的UV。
常用操作
PFADD visitlog 192.168.65.111 192.168.65.112 192.168.65.111 //添加用户访问记录
PFCOUNT visitlog //统计不同的独立访客
其他操作
PFMERGE destkey [sourcekey [sourcekey ...]] //将多个hyperloglong数据整合成一条记录。
Geo
常用操作
GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...] //添加一个或多个地点
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
[ANY]] [ASC|DESC] [STORE key|STOREDIST key] //查询某个经纬度地址附近的地点
GEOSEARCH key FROMMEMBER member|FROMLONLAT longitude latitude BYRADIUS radius
M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [WITHCOORD]
[WITHDIST] [WITHHASH] //查询某个地点附近的地点
应用场景
• 获取经纬度
https://api.map.baidu.com/lbsapi/getpoint/index.html • 添加商家地址
GEOADD changsha 113.017489 28.200454 火车站
112.96903 28.201195 橘子洲 113.017031 28.199706 赛格广
场 113.017004 28.197677 国储
• 查询距离
GEODIST changsha 火车站 橘子洲 M
• 查找火车站附近的景点
GEORADIUSBYMEMBER changsha 火车站 2 KM withdist
withcoord count 4 withhas
Stream
Redis版的MQ -- 阻塞队列 + pub/sub
了解即可,企业应用比较少。
线程模型
Redis到底是单线程还是多线程?
首先:整体来说,Redis的整体线程模型可以简单解释为 客户端多线程,服务端单线程
服务端,Redis响应网络IO和键值对读写的请求,则是由一个单独的主线程完成的。Redis基于epoll实现了IO多路复用,这就可以用一个主线程同时响应多个客户端Socket连接的请求。
在这种线程模型下,Redis将客户端多个并发的请求转成了串行的执行方式。因此,在Redis中,完全不用考虑诸如MySQL的脏读、幻读、不可重复读之类的并发问题。并且,这种串行化的线程模型,加上Redis基于内存工作的极高性能,也让Redis成为很多并发问题的解决工具。
如何保证指令原子性
复合指令
Redis内部提供了很多复合指令,他们是一个指令,可是明显干着多个指令的活。 比如 MSET(HMSET)、GETSET、SETNX、SETEX。这些复合指令都能很好的保持原子性。
Redis事务
Redis的事务并不是像数据库的事务那样,保证事务中的指令一起成功或者一起失败。Redis的事务作用,仅仅只是保证事务中的原子操作是一起执行,而不会在执行过程中被其他指令加塞。
Pipeline
如果你有大批量的数据需要快速写入到Redis中,这种方式可以一定程度提高执行效率
核心作用:优化RTT(round-trip time)
RTT是什么?
当客户端执行一个指令,数据包需要通过网络从Client传到Server,然后再从Server返回到Client。这个中间的时间消耗,就称为RTT(Rount Trip Time)。
Redis的原生复合指令和事务,都是原子性的。但是pipeline不具备原子性。pipeline只是将多条命令发送到服务端,最终还是可能会被其他客户端的指令加塞的,虽然这种概率通常比较小。所以在pipeline中通常不建议进行复杂的数据操作。同时,这也表明,执行复合指令和事务,会阻塞其他命令执行,而执行pipeline不会。
pipeline的执行需要客户端和服务端同时完成,pipeline在执行过程中,会阻塞当前客户端。在pipeline中不建议拼装过多的指令。因为指令过多,会使客户端阻塞时间太长,同时服务端需要回复这个很繁忙的客户端,占用很多内存。
总体来说,pipeline机制适合做一些在非热点时段进行的数据调整任务。
lua脚本
lua是一种小巧的脚本语言,他拥有很多高级语言的特性。比如参数类型、作用域、函数等。
注意点
1、 不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令。相比之下,管道pipeline不会阻塞redis。
Redis中有一个配置参数来控制Lua脚本的最长控制时间。默认5秒钟。当lua脚本执行时间超过了这个时长,Redis会对其他操作返回一个BUSY错误,而不会一直阻塞。
2、尽量使用只读脚本
只读脚本是Redis7中新增的一种脚本执行方法,表示那些不修改Redis数据集的只读脚本。需要在脚本上加上一个只读的标志,并通过指令EVAL_RO触发。在只读脚本中不允许执行任何修改数据集的操作,并且可以随时使用SCRIPT_KILL指令停止。
使用只读脚本的好处一方面在于可以限制某些用户的操作。另一方面,这些只读脚本通常都可以转移到备份节点执行,从而减轻Redis的压力。
3、热点脚本可以缓存到服务端
Redis Function
什么是Function
Redis Function允许将一些功能声明成一个统一的函数,提前加载到Redis服务端(可以由熟悉Redis的管理员加载)。客户端可以直接调用这些函数,而不需要再去开发函数的具体实现。
Redis Function更大的好处在于在Function中可以嵌套调用其他Function,从而更有利于代码复用。相比之下,lua脚本就无法进行复用。
Function注意点
1、Function同样也可以进行只读调用。 
2、如果在集群中使用Function,目前版本需要在各个节点都手动加载一次。Redis不会在集群中进行Function同步
3、Function是要在服务端缓存的,所以不建议使用太多太大的Function。
4、Function和Script一样,也有一系列的管理指令。使用指令 help @scripting 自行了解。
总结
在这几种工具中,Lua脚本通常会是项目中用得最多的方式。在很多追求极致性能的高并发场景,Lua脚本都会担任很重要的角色。
Bigkey问题
Bigkey指那些占用空间非常大的key。比如一个list中包含200W个元素,或者一个string里放一篇文章。基于Redis的单线程为主的核心工作机制,这些Bigkey非常容易造成Redis的服务阻塞。
在Redis客户端指令中,提供了两个扩展参数,可以帮助快速发现这些BigKey
--bigkeys Sample Redis keys looking for keys with many elements (complexity).
--memkeys Sample Redis keys looking for keys consuming a lot of memory.
总结
Redis的线程模型整体还是多线程的,只是后台执行指令的核心线程是单线程的。整个线程模型可以理解为还是以单线程为主。基于这种单线程为主的线程模型,不同客户端的各种指令都需要依次排队执行。
Redis这种以单线程为主的线程模型,相比其他中间件,还是非常简单的。这使得Redis处理线程并发问题,要简单高效很多。甚至在很多复杂业务场景下,Redis都是用来进行线程并发控制的很好的工具。但是,这并不意味着Redis就没有线程并发的问题。这时候选择合理的指令执行方式,就非常重要了。
另外,Redis这种比较简单的线程模型其实本身是不利于发挥多线程的并发优势的。而且Redis的应用场景又通常与高性能深度绑定在一起,所以,在使用Redis的时候,还是要时刻思考Redis的这些指令执行方式,这样才能最大限度发挥Redis高性能的优势。
数据安全性分析
性能压测脚本介绍
Redis提供了压测脚本redis-benchmark,可以对Redis进行快速的基准测试。
# 20个线程,100W个请求,测试redis的set指令(写数据)
redis-benchmark -a Zero156423 -t set -n 1000000 -c 20
数据持久化机制详解
Redis提供了很多跟数据持久化相关的配置,大体上,可以组成以下几种策略:
无持久化:完全关闭数据持久化,不保证数据安全。相当于将Redis完全当做缓存来用
RDB(RedisDatabase):按照一定的时间间隔缓存Redis所有数据快照。
AOF(Append Only File):记录Redis收到的每一次写操作。这样可以通过操作重演的方式恢复Redis的数据
RDB+AOF:同时保存Redis的数据和操作。
RDB
优点
1、RDB文件非常紧凑,非常适合定期备份数据。
2、RDB快照非常适合灾难恢复。
3、RDB备份时性能非常快,对主线程的性能几乎没有影响。RDB备份时,主线程只需要启动一个负责数据备份的子线程即可。所有的备份工作都由子线程完成,这对主线程的IO性能几乎没有影响。
4、与AOF相比,RDB在进行大数据量重启时会快很多。
2、RDB快照非常适合灾难恢复。
3、RDB备份时性能非常快,对主线程的性能几乎没有影响。RDB备份时,主线程只需要启动一个负责数据备份的子线程即可。所有的备份工作都由子线程完成,这对主线程的IO性能几乎没有影响。
4、与AOF相比,RDB在进行大数据量重启时会快很多。
缺点
1、RDB不能对数据进行实时备份,所以,总会有数据丢失的可能。
2、RDB需要fork化子线程的数据写入情况,在fork的过程中,需要将内存中的数据克隆一份。如果数据量太大,或者CPU性能不是很好,RDB方式就容易造成Redis短暂的服务停用。相比之下,AOF也需要进行持久化,但频率较低。并且你可以调整日志重写的频率。
2、RDB需要fork化子线程的数据写入情况,在fork的过程中,需要将内存中的数据克隆一份。如果数据量太大,或者CPU性能不是很好,RDB方式就容易造成Redis短暂的服务停用。相比之下,AOF也需要进行持久化,但频率较低。并且你可以调整日志重写的频率。
AOF
优点
1、AOF持久化更安全。例如Redis默认每秒进行一次AOF写入,这样,即使服务崩溃,最多损失一秒的操作。
2、AOF的记录方式是在之前基础上每次追加新的操作。因此AOF不会出现记录不完整的情况。即使因为一些特殊原因,造成一个操作没有记录完整,也可以使用redis-check-aof工具轻松恢复。
3、当AOF文件太大时,Redis会自动切换新的日志文件。这样就可以防止单个文件太大的问题。
4、AOF记录操作的方式非常简单易懂,你可以很轻松的自行调整日志。比如,如果你错误的执行了一次 FLUSHALL 操作,将数据误删除了。使用AOF,你可以简单的将日志中最后一条FLUSHALL指令删掉,然后重启数据库,就可以恢复所有数据。
2、AOF的记录方式是在之前基础上每次追加新的操作。因此AOF不会出现记录不完整的情况。即使因为一些特殊原因,造成一个操作没有记录完整,也可以使用redis-check-aof工具轻松恢复。
3、当AOF文件太大时,Redis会自动切换新的日志文件。这样就可以防止单个文件太大的问题。
4、AOF记录操作的方式非常简单易懂,你可以很轻松的自行调整日志。比如,如果你错误的执行了一次 FLUSHALL 操作,将数据误删除了。使用AOF,你可以简单的将日志中最后一条FLUSHALL指令删掉,然后重启数据库,就可以恢复所有数据。
缺点
1、针对同样的数据集,AOF文件通常比RDB文件更大。
2、在写操作频繁的情况下,AOF备份的性能通常比RDB更慢。
2、在写操作频繁的情况下,AOF备份的性能通常比RDB更慢。
整体使用建议
1、如果你只是把Redis当做一个缓存来用,可以直接关闭持久化。
2、如果你更关注数据安全性,并且可以接受服务异常宕机时的小部分数据损失,那么可以简单的使用RDB策略。这样性能是比较高的。
3、不建议单独使用AOF。RDB配合AOF,可以让数据恢复的过程更快。
RDB详解
RDB可以在指定的时间间隔,备份当前时间点的内存中的全部数据集,并保存到餐盘文件当中。通常是dump.rdb文件。在恢复时,再将磁盘中的快照文件直接都会到内存里。
相关重要配置
save策略: 核心配置
dir 文件目录
dbfilename 文件名 默认dump.rdb
rdbcompression 是否启用RDB压缩,默认yes。 如果不想消耗CPU进行压缩,可以设置为no
stop-writes-oin-bgsave-error 默认yes。如果配置成no,表示你不在乎数据不一致或者有其他的手段发现和控制这种不一致。在快照写入失败时,也能确保redis继续接受新的写入请求。
rdbchecksum 默认yes。在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗。如果希望获得最大的性能提升,可以关闭此功能。
dir 文件目录
dbfilename 文件名 默认dump.rdb
rdbcompression 是否启用RDB压缩,默认yes。 如果不想消耗CPU进行压缩,可以设置为no
stop-writes-oin-bgsave-error 默认yes。如果配置成no,表示你不在乎数据不一致或者有其他的手段发现和控制这种不一致。在快照写入失败时,也能确保redis继续接受新的写入请求。
rdbchecksum 默认yes。在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗。如果希望获得最大的性能提升,可以关闭此功能。
何时会触发RDB备份
1、到达配置文件中默认的快照配置时,会自动触发RDB快照
2、手动执行save或者bgsave指令时,会触发RDB快照。 其中save方法会在备份期间阻塞主线程。bgsve则不会阻塞主线程。但是他会fork一个子线程进行持久化,这个过程中会要将数据复制一份,因此会占用更多内存和CPU。
3、主从复制时会触发RDB备份。
AOF详解
以日志的形式记录每个写操作(读操作不记录)。只允许追加文件而不允许改写文件。
相关重要配置
appendonly 是否开启aof。 默认是不开启的。
appendfilename 文件名称。
appendfsync 同步方式。默认everysecond 每秒记录一次。no 不记录(交由操作系统进行内存刷盘)。 always 记录每次操作,数据更安全,但性能较低。
appenddirname AOF文件目录。新增参数,指定aof日志的文件目录。 实际目录是 {dir}+{appenddirname}
auto-aof-rewrite-percentage, auto-aof-rewrite-min-size 文件重写触发策略。默认每个文件64M, 写到100%,进行一次重写。
no-appendfsync-on-rewrite aof重写期间是否同步
AOF日志恢复
如果Redis服务出现一些意外情况,就会造成AOF日志中指令记录不完整。这时,将Redis服务重启,就会发现重启失败。这时就需要先将日志文件修复,然后才能启动。
redis-check-aof --fix appendonly.aof.1.incr.aof
混合持久化策略
RDB和AOF两种持久化策略各有优劣,所以在使用Redis时,是支持同时开启两种持久化策略的。在redis.conf配置文件中,有一个参数可以同时打开RDB和AOF两种持久化策略。
aof-use-rdb-preamble yes
主从复制Replica机制详解
主从复制。当Master数据有变化时,自动将新的数据异步同步到其他slave中。
最典型的作用:
● 读写分离:mater以写为主,Slave以读为主
● 数据备份+容灾恢复
如何配置Replica
配置方式在基础课程部分有详细讲解,这里不做过多重复。简单总结一个原则:配从不配主。 这意味着对于一个Redis服务,可以在几乎没有影响的情况下,给他配置一个或者多个从节点。
相关核心操作简化为以下几点:
● REPLICAOF host port|NO ONE : 一般配置到redis.conf中。
● SLAVEOF host port|NO ONE: 在运行期间修改slave节点的信息。如果该服务已经是某个主库的从库了,那么就会停止和原master的同步关系。
如何确定主从状态?从库可以写数据吗?
主从状态可以通过 info replication查看。
默认情况下,从库是只读的,不允许写入数据。因为数据只能从master往slave同步,如果slave修改数据,就会造成数据不一致。
虽然禁止了对数据的写操作,但是并没有禁止CONFIG、DEBUG等管理指令,这些指令如果和主节点不一致,还是容易造成数据不一致。如果为了安全起见,可以使用rename-command方法屏蔽这些危险的指令。
例如在redis.conf配置文件中增加配置 rename-command CONFIG "" 。就可以屏蔽掉slave上的CONFIG指令。
主从复制工作流程
1》 Slave启动后,向master发送一个sync请求。等待建立成功后,slave会删除掉自己的数据日志文件,等待主节点同步。
2》master接收到slave的sync请求后,会触发一次RDB全量备份,同时收集所有接收到的修改数据的指令。然后master将RDB和操作指令全量同步给slave。完成第一次全量同步。
3》主从关系建立后,master会定期向slave发送心跳包,确认slave的状态。心跳发送的间隔通过参数repl-ping-replica-period指定。默认10秒。
4》只要slave定期向master回复心跳请求,master就会持续将后续收集到的修改数据的指令传递给slave。同时,master会记录offset,即已经同步给slave的消息偏移量。
5》如果slave短暂不回复master的心跳请求,master就会停止向slave同步数据。直到slave重新上线后,master从offset开始,继续向slave同步数据。
主从复制的缺点
1》复制延时,信号衰减: 所有写操作都是先在master上操作,然后再同步到slave,所以数据同步一定会有延迟。当系统繁忙,或者slave数量增加时,这个延迟会更加严重。
2》master高可用问题: 如果master挂了,slave节点是不会自动切换master的,只能等待人工干预,重启master服务,或者调整主从关系,将一个slave切换成master,同时将其他slave的主节点调整为新的master。
后续的哨兵集群,就相当于做这个人工干预的工作。当检测到master挂了之后,自动从slave中选择一个节点,切换成master。
3》从数据安全性的角度,主从复制牺牲了服务高可用,但是增加了数据安全。
哨兵集群Sentinel机制详解
Sentinel是什么?有什么用
Redis的Sentinel不负责数据读写,主要就是给Redis的Replica主从复制提供高可用功能。主要作用有四个:
● 主从监控:监控主从Redis运行是否正常
● 消息通知:将故障转移的结果发送给客户端
● 故障转移:如果master异常,则会进行主从切换。将其中一个slave切换成为master。
● 配置中心:客户端通过连接哨兵可以获取当前Redis服务的master地址。
核心配置
Sentinel最核心的配置其实就是 sentinel.conf中的sentinel monitor <master-name> <ip> <redis-port> <quorum>这个配置中,最抽象的参数就最后的那个quorum。这个参数是什么意思呢?这就需要了解一下Sentinel的工作原理。
Sentinel工作原理
如何发现master服务宕机
S_DOWN(主观下线)和 O_DOWN(客观下线)
对于每一Sentinel服务,他会不断地往master发送心跳,监听master的状态。如果经过一段时间(参数sentinel down-after-milliseconds <master-name> <milliseconds> 指定。默认30秒)没有收到master的响应,他就会主观的认为这个master服务下线了。也就是S_DOWN。
但是主观下线并不一定是master服务的问题,如果网络出现抖动或者阻塞,也会造成master的响应超时。为了防止网络抖动造成的误判,Redis的Sentinel就会互相进行沟通,当超过quorum个Sentinel节点都认为master已经出现S_DOWN后,就会将master标记为O_DOWN。此时才会真正确定master的服务是宕机的,然后就可以开始故障切换了。
在配置Sentinel集群时,通常都会搭建奇数个节点,而将quorum配置为集群中的过半个数。这样可以最大化的保证Sentinel集群的可用性。
发现master服务宕机后,如何切换新的master
master变成O_DOWN后,Sentinel会在集群中选举产生一个服务节点作为Leader。Leader将负责向其他Redis节点发送命令,协调整个故障切换过程。在选举过程中,Sentinel是采用的Raft算法,这是一种多数派统一的机制,其基础思想是对集群中的重大决议,只要集群中超过半数的节点投票同意,那么这个决议就会成为整个集群的最终决议。
Sentinel会在剩余健康的Slave节点中选举出一个节点作为新的Master。 选举的规则如下:
● 首先检查是否有提前配置的优先节点:各个服务节点的redis.conf中的replica-priority配置最低的从节点。这个配置的默认值是100。如果大家的配置都一样,就进入下一个检查规则。
● 然后检查复制偏移量offset最大的从节点。也就是找同步数据最快的slave节点。因为他的数据是最全的。如果大家的offset还是一样的,就进入下一个规则
● 最后按照slave的RunID字典顺序最小的节点。
切换新的主节点。 Sentinel Leader给新的mater节点执行 slave of no one操作,将他提升为master节点。 然后给其他slave发送slave of 指令。让其他slave成为新Master的slave。
如果旧的master恢复了,Sentinel Leader会让旧的master降级为slave,并从新的master上同步数据,恢复工作。
缺点
对客户端不太友好
数据不安全
集群Cluster机制详解
将多组Redis Replica主从集群整合到一起,像一个Redis服务一样对外提供服务。;所以Redis Cluster的核心依然是Replica复制集。
Redis Cluster通过对复制集进行合理整合后,核心是要解决三个问题:
1》 客户端需要频繁切换master的问题。
2》服务端数据量太大后,单个复制集难以承担的问题。
3》master节点挂了之后,主动将slave切换成master,保证服务稳定
核心配置
配置文件示例
# 允许所有的IP地址
bind * -::*
# 后台运行
daemonize yes
# 允许远程连接
protected-mode no
# 密码
requirepass 123qweasd
# 主节点密码
masterauth 123qweasd
# 端口
port 6381
# 开启集群模式
cluster-enabled yes
# 集群配置文件
cluster-config-file nodes-6381.conf
# 集群节点超时时间
cluster-node-timeout 5000
# log日志
logfile "/root/myredis/cluster/redis6381.log"
# pid文件
pidfile /var/run/redis_6381.pid
# 开启AOF持久化
appendonly yes
# 配置数据存储目录
dir "/root/myredis/cluster"
# AOF目录
appenddirname "aof"
# AOF文件名
appendfilename "appendonly6381.aof"
# RBD文件名
dbfilename "dump6381.rdb"
接下来依次创建6381,6382,6383,6384,6385,6386六个端口的Redis配置文件,并启动服务。
redis-cli -a 123qweasd --cluster create --cluster-replicas 1 192.168.65.214:6381 192.168.65.214:6382 192.168.65.214:6383 192.168.65.214:6384 192.168.65.214:6385 192.168.65.214:6386
其中 --cluster create表示创建集群。 --cluster-replicas 表示为每个master创建一个slave节点。接下来,Redis会自动分配主从关系,形成Redis集群。
集群启动完成后,可以使用客户端连接上其中任意一个服务端,验证集群。
详解Slot槽位
Redis集群设置16384个哈希槽。每个key会通过CRC16校验后,对16384取模,来决定放到哪个槽。集群的每个节点负责一部分的hash槽。
Slot如何分配
Redis集群中内置16384个槽位。在建立集群时,Redis会根据集群节点数量,将这些槽位尽量平均的分配到各个节点上。并且,如果集群中的节点数量发生了变化。(增加了节点或者减少了节点)。就需要触发一次reshard,重新分配槽位。而槽位中对应的key,也会随着进行数据迁移。
如何确定key与slot的对应关系?
dis集群中,对于每一个要写入的key,都会寻找所属的槽位。计算的方式是 CRC16(key) mod 16384。
首先,这意味着在集群当中,那些批量操作的复合指令(如mset,mhset)支持会不太好。如果他们分属不同的槽位,就无法保证他们能够在一个服务上进行原子性操作。
然后,在Redis中,提供了指令 CLUSTER KEYSLOT 来计算某一个key属于哪个Slot
首先,这意味着在集群当中,那些批量操作的复合指令(如mset,mhset)支持会不太好。如果他们分属不同的槽位,就无法保证他们能够在一个服务上进行原子性操作。
然后,在Redis中,提供了指令 CLUSTER KEYSLOT 来计算某一个key属于哪个Slot
集群能不能保证数据安全?
在Redis集群相对比较稳定的时候,Redis集群是能够保证数据安全的。
由于Redis集群的gossip协议在同步元数据时不保证强一致性,这意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令。
总结
对于任何数据存储系统来说,数据安全都是重中之重。Redis也不例外。从数据安全性的角度来梳理Redis从单机到集群的各种部署架构,可以看到用Redis保存数据基本上还是非常靠谱的。甚至Redis的数据保存策略,在很多场景下,都是一种教科书级别的解决方案。
高并发分布式锁
秒杀抢购场景下实战JVM级别锁与分布式锁
高并发情形下依然存在超卖问题,线程 1 执行时间超过过期时间可能将线程 2 刚加的锁删除
解决方案:定时任务监控主线程业务,业务没有结束延长锁的过期时间
Redisson框架实战
String lockKey = "lock:product_101";
//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
//stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
/*String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
if (!result) {
return "error_code";
}*/
//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁
redissonLock.lock(); // .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
/*if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}*/
//解锁
redissonLock.unlock();
}
Lua脚本语言快速入门与使用注意事项
使用脚本的好处如下:
1、减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。这点跟管道类似。
2、原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过redis的批量操作命令(类似mset)是原子的。
3、替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,官方推荐如果要使用redis的事务功能可以用redis lua替代。
Redisson分布式锁源码剖析
Redis主从架构锁失效问题
主节点加锁后,未完成同步锁信息到从节点宕机,从节点升级为主节点,锁信息失效-锁丢失
从CAP角度剖析Redis与Zookeeper分布式锁区别
Redis:AP;Zookeeper:CP
Redis:通知线程加锁成功,再同步从节点【存在宕机,锁信息失效】
Zookeeper:先同步锁信息到从节点,有半数以上节点同步成功,再通知线程加锁成功【存在性能问题】
如何将分布式锁性能提升100倍
1、加锁粒度小
2、分段锁:分段加锁【参考 ConcurrentHashMap】
高并发缓存架构实战与性能优化
缓存设计与性能优化
缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
第一, 自身业务代码或者数据出现问题。
第二, 一些恶意攻击、 爬虫等造成大量空命中。
缓存穿透问题解决方案:
1、缓存空对象
2、布隆过滤器
布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。
向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组长度比较大,存在概率就会很大,如果这个位数组长度比较小,存在概率就会降低。
这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少
注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。
缓存失效(击穿)
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。
缓存雪崩
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。
由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
预防和解决缓存雪崩问题, 可以从以下三个方面进行着手。
1) 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
2) 依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。
比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
热点缓存key重建优化
开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害
○ 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
○ 重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。
在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。
要解决这个问题主要就是要避免大量线程同时重建缓存。
我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
缓存与数据库双写不一致
1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
3、如果不能容忍缓存数据不一致,可以通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。
4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。
Redis Stack扩展
Redis JSON
RedisJSON是Redis的一个扩展模块,它提供了对JSON数据的原生支持。通过RedisJSON,我们可以将JSON数据直接存储在Redis中,并利用丰富的命令集进行高效的查询和操作。RedisJSON不仅简化了数据处理的流程,还大幅提升了处理JSON数据的性能。
Redis JSON模块为Redis添加了JSON数据类型的支持,并且对JSON数据提供了快速进行增、删、改、查的操作。
优势
● Redis JSON存储数据的性能更高。Redis JSON底层其实是以一种高效的二进制的格式存储。相比简单的文本格式,二进制格式进行JOSN格式读写的性能更高,也更节省内存。根据官网的性能测试报告,使用Redis JSON读写JSON数据,性能已经能够媲美MongoDB以及ElasticSearch等传统NoSQL数据库。
● Redis JSON使用树状结构来存储JSON。这种存储方式可以快速访问子元素。与传统的文本存储方案相比,树状存储结构能够更高效的执行查询操作。
● 与Redis生态集成度高。作为Redis的扩展模块,Redis JSON和Redis的其他功能和工具无缝集成。这意味着开发者可以继续使用TTL、Redis事务、发布/订阅、Lua脚本等功能。
Search And Query
当Redis中存储的数据比较多时,搜索Redis中的数据是一件比较麻烦的事情。通常使用的 keys * 这样的指令,在生产环境一般都是直接禁用的,因为这样会产生严重的线程阻塞,影响其他的读写操作。
如何快速搜索Redis中的数据(主要是key)呢? Redis中原生提供了Scan指令,另外在Redis Stack中也增加了Search And Query模块。
传统Scan搜索
Scan指令的基础思想就是每次只返回想要查询的一部分结果数据,然后通过迭代的方式,逐步返回完整数据。
scan指令的基础使用方式:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
这几个核心参数介绍如下:
● cursor: 游标。代表每次迭代返回的偏移量。通常一次查询,cursor从0开始,然后scan指令会返回下一次迭代的起始偏移量。用户可以用这个返回值作为cursor,继续迭代下一批。直到cursor返回0,表示所有数据都过滤完成了。
● pattern:匹配字符串。用来匹配要查询的key。 例如 user* 表示以user开头的字符串。
● count:数字,表示每次迭代多少条数据。
● type是key的类型,比如可以指定string ,set,zset等。
Search And Query搜索
Redis提供了RedisSearch插件,基本就可以认为是ElasticSearch这类搜索引擎的平替。大部分ES能够实现的搜索功能,在Redis里就能直接进行。这样就极大的减少了数据迁移带来的麻烦。
Bloom Filter
一句话解释:一种快速检索一个元素是否在一个海量集合中的算法。
布隆过滤器判断一个元素不在集合中,那么这个元素肯定不在集合中。但是,布隆过滤器判断一个元素在集合中,那么这个元素有可能不在集合中。
Guava的布隆过滤器示例
布隆过滤器中,将一个原本不在集合中的元素判断成为在集合中,这就是误判。而误判率是布隆过滤器一个很重要的控制指标。
在算法实现时,误判率是可以通过设定更复杂的哈希函数组合以及做更大的位数组来进行控制的。所以,在布隆过滤器的初始化过程中,通常只需要指定过滤器的容量和误判率,就足够了。
public static void main(String[] args) {
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),10000,0.01);
//把 A~Z 放入布隆过滤器
for (int i = 64; i <= 90 ; i++) {
bloomFilter.put(String.valueOf((char) i));
}
System.out.println(bloomFilter.mightContain("A")); //true
System.out.println(bloomFilter.mightContain("a")); //false
}
布隆过滤器是用的二进制数组来保存数据,所以,Redis的BitMap数据结构天生就非常适合做一个分布式的布隆过滤器底层存储。只是算法还是需要自己实现。
Cuckoo Filter
布隆过滤器最大的问题是无法删除数据。因此,后续诞生了很多布隆过滤器的改进版本。Cuckoo Filter 布谷鸟过滤器就是其中一种。
相比于布隆过滤器,Cuckoo Filter可以删除数据。而且基于相同的集合和误报率,Cuckoo Filter通常占用空间更少。相对的,算法实现也就更复杂。
不过他同样有误判率。即有可能将一个不在集合中的元素错误的判断成在集合中。布隆过滤器的误报率通过调整位数组的大小和哈希函数来控制,而CuckooFilter的误报率受指纹大小和桶大小控制。
使用示例
-- 创建默认值
## 容量1000,这个是必填参数。后面几个都是可选参数。这里填的几个就是Redis中的CuckooFilter的默认值
## BUSKETSIZE越大,空间利用率更高,但是误判率也更高,性能更差
## MAXITARATIONS越小,性能越好。如果设置越大,空间利用率就越好。
## EXPANSION 是指空间扩容的比例。
CF.RESERVE cf 1000 BUSKETSIZE 2 MAXITERATIONS 20 EXPANSION 1
底层数据结构解析
Redis底层数据结构
String数据结构
● int : 如果value可以转换成一个long类型的数字,那么就用int保存value。只有整数才会使用int,如果是浮点数,Redis内部其实是先将浮点数转化成字符串,然后保存
● embstr : 如果value是一个字符串类型,并且长度小于44字节的字符串,那么Redis就会用embstr保存。代表embstr的底层数据结构是SDS(Simple Dynamic String 简单动态字符串)
● raw :如果value是一个字符串类型,并且长度大于44字节,就会用raw保存。
HASH类型数据结构
底层数据结构详解
hash类型的数据,底层存储时,有两种存储格式。hashtable和listpack
就是hash型的数据,如果value里的数据比较少,就用listpack。如果数据比较多,就用hashtable。
如何判断value里的数据少,涉及到两个参数。hash-max-listpack-entries 限制value里键值对的个数(默认512),hash-max-listpack-value 限制value里值的数据大小(默认64字节)。
从这两个参数里可以看到,对于hash类型数据,大部分正常情况下,都是使用listpack。
listpack
对于一个ziplist,要找到对列的第一个元素和最后一个元素,都是比较容易的,可以通过头部的三个字段直接找到。但是,如果想要找到中间某一些元素(比如Redis 的list数据类型的LRANGE指令),那么就只能依次遍历(从前往后单向遍历)。所以,ziplist不太适合存储太多的元素。
总结
1、hash底层更多的是使用listpack来存储value。
2、如果hash对象保存的键值对超过512个,或者所有键值对的字符串长度超过64字节,底层的数据结构就会由listpack升级成为hashtable。
3、对于同一个hash数据,listpack结构可以升级为hashtable结构,但是hashtable结构不会降级成为listpack。
List类型数据结构
对于list数据类型,Redis是根据value中数据的大小判断底层数据结构的。数据比较“小”的list类型,底层用listpack保存。数据量比较"大"的list类型,底层用quicklist保存。
quicklist简介
listpack可以看成是一个数组(Array)结构。而对于数据结构,他的好处是存储数据是连续的,所以,对数组中的数据进行检索是比较快的,通过偏移量就可以快速定位。listpack的这种结构非常适合支持Redis的list数据类型的LRANGE这样的检索操作。
但是,对于数组来说,他的数据节点修改就会比较麻烦。 每次新增或者删除一个节点,都需要调整大量节点的位置。这又使得listpack的数据结构对于Redis的list数据类型的LPUSH这样增加节点的操作非常不友好。尤其当list中的数据节点越多,LPUSH这样的操作要移动的内存也就会越多。
总结
如果list的底层数据量比较小时,Redis底层用listpack结构保存。当list的底层数据量比较大时,Redis底层用quicklist结构保存。
至于这其中数据量大小的判断标准,由参数list-max-listpack-size决定。这个参数设置成正数,就是按照list结构的数据节点个数判断。负数从-1到-5,就是按照数据节点的大小判断。
SET类型数据结构
Redis底层综合使用intset+listpack+hashtable存储set数据。set数据的子元素也是<k,v>形式的entry。其中,key就是元素的值,value是null。
底层数据结构详解
set底层的intset,listpack,hashtable这三种数据类型。intset,其实是一种比较简单的数据结构。就是保存一个整数。
ZSET类型数据结构
Redis底层综合使用listpack + skiplist两种结构来保存zset类型的数据。
数据结构详解
zset类型的数据,底层需要先按照score进行排序。排序过程中是需要移动内存的。如果节点数据不是太多,将这些内存移动完后,重新整理成一个类似数据Array的listpack结果是可以接受的。但是如果数据量太大(节点数和数据大小),那么频繁移动内存,开销就比较大了。这时,显然以链表这种零散的数据结构是比较合适的。
但是,对于一个单链表结构来说,要检索链表中的某一个数据,只能从头到尾遍历链表。时间复杂度是O(N),性能是比较低的。
skiplist
skiplist是一种典型的用空间换时间的解决方案,优点是数据检索的性能比较高。时间复杂度是O(logN),空间复杂度是O(N)。但是他的缺点也很明显,就是更新链表时,维护索引的成本相对更高。因此,skiplist适合那些数据量比较大,且是读多写少的应用场景。
总结
Redis底层综合使用listpack+skiplist两种数据结构来保存zset类型的数据。其中,当zset数据的value数据量比较小时,使用listpack结构保存。value数据量比较大时,使用skiplist结构保存。skiplist是一种典型的用空间换时间的解决方案,适合那些数据量比较大,且读多写少的数据场景。在Redis中使用是非常合适的。
Redis中衡量zset的value数据大小的参数有两个,zset-max-listpack-entries 和 zset-max-listpack-value 分别从value的元数数量和数据大小两方面进行区分。
0 条评论
下一页