# 使用场景
高性能
提高后续请求的响应时间
高并发
分流请求,将大部分请求路由至缓存处理
# 数据类型
# String
# 数据要求
键值可以为任何数据(最大为512M)【如jpeg图像或序列化的对象】
# 常用操作
# 增删改查
> set <key> <value>
OK
> get <key>
<value> / (nil)
> del <key>
(Integer) 1/(Integer) 0
> mset <key1> <value1> <key2> <value2> ...
> mget <key1> <key2>
# 字符串操作
> strlen <key>
> append <key>
> getrange <key> <pos1> <pos2> # 双闭区间截取(java中对应方法为开闭截取)
# 特殊操作
数值自增
> incr <key> > incrby <key> <increment> > incrbyfloat <key> <increment>
若数据不能转换为数值,或运算结果超出
Long.MAX_VALUE
,则报错设置数据生命周期
> setex <key> <ttl> <val>
> psetex <key> <ttl> <val>
java.String.substring()
方法为左闭右开区间截取
# 应用场景
多用于数据整体存取
存储结构型或非结构型热点数据
数据key命名规范:
set <表名>:<主键名>:<主键值> <多字段JSON对象>
set <表名>:<主键名>:<主键值>:<字段名> <字段值>
(性能较优)
用作RDBMS分表主键生成策略(分批生成,无需考虑并发问题)
# Hash
# 数据要求
底层为哈希表(field
-value
),哈希值只能为字符串
# 常用操作
# 增删改查
> hset <key> <field> <value>
(Integer) 1 / (Integer) 0 # 1表示field数目变化,0表示不变(不论失败与否)
> hget <key> <field>
<value> / (nil)
> hdel <key> <field1> [field2] ...
(Integer) 1 / (Integer) 0
> hgetall <key>
> hmset <key> <field1> <value1> <field2> <value2> ...
> hmget <key> <field1> <field2> ...
# 集合操作
> hlen <key>
(Integer) x
> hexists <key> <field>
(Integer) 1 / (Integer) 0
> hkeys <key>
> hvals <key>
> hsetnx <key> <field> <value>
(Integer) 1 / (Integer) 0 # 1表示成功,0表示失败
# 特殊操作
- 数值自增
> hincrby <key> <field> <increment>
> hincrbyfloat <key> <field> <increment>
# 注意事项
- 相比于String操作,内存与CPU消耗更低
- Hash中每个key最多可以存储2^32-1个键值对,不可将Hash作为对象列表滥用
- 当Hash中key未达到阈值或单个元素较小时,底层为Ziplist,按插入顺序进行紧密存储;若Hash中key过多,底层转换为哈希,数据改为无序排列,遍历所有field的效率很低,此时应将key进行分片存储
# 应用场景
多用于同类数据的归类整合与数据的局部更新
存储购物车数据
以用户id为key,针对每位用户创建Hash,并以商品id作为field,商品加车数量作为value进行存储
同时额外维护商品信息Hash进行解耦,使用
setnx
更新商品信息存储抢购、限购时的商品数量
以商家id为key,商品id作为field,商品剩余数量作为value进行存储
超卖问题
# List
# 数据要求
底层为双向链表,元素必须为字符串;可实现双端队列操作
# 常用操作
# 增删改查
> lpush <key> <value1> [<value2> ...]
(Integer) x
> rpush <key> <value1> [<value2> ...]
(Integer) x
> lrange <key> <start> <end> # end为-1时遍历全部元素
> lindex <key> <index>
<value> / (nil)
> lpop <key>
<value> / (nil)
> rpop <key>
<value> / (nil)
> lrem <key> <count> <value>
(Integer) x
# 集合操作
> llen <key>
(Integer) x
# 特殊操作
- 阻塞式出列
> blpop <key> [<key2> ...] <timeout>
> brpop <key> [<key2> ...] <timeout>
阻塞轮询场景下的异常处理
brpoplpush
将目标list中数据出队,并加入备份list- 发生异常时,
blpoprpush
将备份list中数据还原回目标list;不发生异常时,blpop
移除备份list中数据
# 注意事项
- 作为链表,数据读取速度较慢
- 每个key最多可以存储2^32-1个元素
# 应用场景
存储朋友圈点赞顺序、社交网站关注列表信息等
使用
rpush
添加点赞,lrem
移除点赞,lrange
遍历点赞名单,lindex
获取点赞状态(用户是否已点赞)存储信息聚合类网站待推送博文等热点信息
普通博文
(对于每一关注者)使用
rpush
添加待推送博文,lrange
遍历待推送博文大V博文
存储多台服务器先后输出的日志等严格区分操作顺序的数据信息(可持久化)
# Set
# 数据要求
底层为value值为空的Hash(Map<String, null>
);查找元素复杂度为O(1)
# 常用操作
# 增删改查
> sadd <key> <member1> [<member2> ...]
(Integer) 1 / (Integer) 0 # 1表示元素数目变化,0表示不变(不论失败与否)
> smembers <key>
> srem <key> <member1> [<member2> ...]
# 集合操作
> scard <key>
(Integer) x
> sismember <key> <member>
(Integer) 1 / (Integer) 0
特殊操作
移动元素
> smove <src> <dest> <member>
随机获取元素
> srandmember <key> [<count>] > spop <key> [<count>]
交并差集
> sinter <key1> <key2> ... > sunion <key1> <key2> ... > sdiff <key1> <key2> ... # 具有方向性 > sinterstore <dest> <key1> <key2> ... > sunionstore <dest> <key1> <key2> ... > sdiffstore <dest> <key1> <key2> ...
# 注意事项
- 无法插入重复元素
# 应用场景
存储资讯或新媒体类网站用户兴趣等随机推荐信息
系统将热门分类信息存入Set,随机挑选出部分分类结合用户已关注信息合并推送给用户
存储同类信息的关联搜索与深度关联搜索等
共同关注:
sinter
我关注的人也关注TA:
sismember
(大V场景优化)可能关注:
sdiff
用于同类型全局数据的快速去重(如网站UV/IP访问量)与快速定位(如网站黑白名单)
存储抽奖用户信息
sadd
添加抽奖用户,srandmember
/spop
进行抽奖
# ZSet
# 数据要求
底层为额外添加score字段的Set;查找元素复杂度为O(1)
# 常用操作
# 增删改查
> zadd <key> <score> <member> [<score2> <member2> ...] # score值在前,value值在后
> zrange <key> <start> <end> [WITHSCORES] # 按照score值从小到大进行排序
> zrevrange <key> <start> <end> [WITHSCORES]
> zrem <key> <member> [<member2> ...]
> zpopmin <key> [<count>]
> zpopmax <key> [<count>]
# 集合操作
> zcard <key>
> zcount <key> <min> <max>
> sinterstore <dest> <numkeys> <key1> <key2> ... [AGGREGATE SUM|MIN|MAX] # AGGREGATE参数表示交并差集操作后对符合条件元素score值的操作
> sunionstore <dest> <numkeys> <key1> <key2> ... [AGGREGATE SUM|MIN|MAX]
> sdiffstore <dest> <numkeys> <key1> <key2> ... [AGGREGATE SUM|MIN|MAX]
# 特殊操作
- 根据score操作
> zrank <key>
> zrevrank <key>
> zscore <key> <member>
> zincrby <key> <increment> <member>
> zrangebyscore <key> <min> <max> [WITHSCORES] [LIMIT <offset> <count>]
> zrevrangebyscore <key> <max> <min> [WITHSCORES] [LIMIT <offset> <count>]
> zremrangebyrank <key> <start> <end> [WITHSCORES]
> zremrangebyscore <key> <min> <max> [WITHSCORES]
# 注意事项
- 能够插入相同元素,并导致该元素score值更新(返回
(Integer) 0
说明元素数目不变) - score值作为整数,上限为2^64-1;作为浮点数,可能会有精度丢失
# 应用场景
存储排行榜(如热门直播、好友亲密度、热销商品、微博热搜等)数据并进行组合统计(可持久化)
存储基础+增值类服务(如云盘、游戏或观影VIP等)的用户会员(体验会员)信息,或限时服务(如投票、私密讨论、限期优惠券等)信息)
- 限时服务:将过期时间作为score,利用排序功能区分业务处理(如中止服务)的先后顺序,同时过期服务进行移除,并按照不同时间段逐级提升(如将一小时内即将逾期的服务加入ZSet进行处理,全部处理完后再加入下一小时即将逾期的服务)(可持久化)
- 限次服务(无需按服务排序):使用String,通过
setex uid:appid 60 1
初始化使用次数(如60s内使用x次),若get
次数达到x次上限,则停止提供服务,特定时间段过后key过期,重新初始化计数(无需持久化)
基本延时消息队列(将消息发布时间戳作为score)
# 其他应用场景
存储社交类应用所接收的用户消息等基于时间顺序的数据信息(可持久化)
先判断该消息是否在置顶Set中,选择置顶List或普通List,先移除List中的相同key,再重新插入;也可使用ZSet,score值维护消息最后发送的时刻
同时使用String或Hash计数器另行维护消息数目,与List进行同步更新
# Bitmap
应用场景
- 统计日活用户等海量数据
setbit 2020:1021 1 1
记录id(id可进行偏移)为1的用户登录;bitcount 2020:1021 0 -1
统计日活用户量(O(n))
# 通用命令
# 基本操作
# key相关
> type <key>
> exists <key>
> del <key>
> keys <pattern>
> rename <key> <newKey>
> renamenx <key> <newKey>
> sort <key>
# 数据库相关
> select <dbIndex>
> move <key> <dbIndex> # 剪切操作
# 扩展操作
# Key时效性控制
> expire <key> <seconds>
> pexpire <key> <milliseconds>
> expireat <key> <timestamp>
> pexpireat <key> <ptimestamp>
> ttl <key> # -2表示已过期,-1表示永不过期
> pttl <key>
> persist <key>
(Integer) 1 / (Integer) 0
# 持久化
bgsave
原理
fork
cow
# 事务
> MULTI
> EXEC/DISCARD
> WATCH
#
# 读操作
# 缓存穿透
策略:缓存空值
# 缓存雪崩
策略:为缓存key过期时间添加额外的随机增量
# 缓存击穿
本地锁(❌)
根据缓存命中结果决定是否进入同步代码块,且在同步代码块头部再次判断缓存是否命中(双重检查);在退出同步代码块前写入缓存,保证在下一线程进入同步代码块时缓存已同步
# 分布式锁
> SET LOCK_KEY 1 PX <MILLISECONDS> NX # 如果成功获取锁 > ... # 执行业务操作 > DEL LOCK_KEY # 释放锁
过期时间一般为:业务最大耗时✖️120%+平均网络耗时✖️110%(有隐患)
改进:
释放锁之前判断锁是否过期,同时是否是自己所设置的锁(如LOCK_KEY值为UUID),通过lua脚本保证该流程为原子操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
# 写操作
# 分布式锁
- 互斥/排他性:在任意时刻,只有一个客户端能持有锁(
setnx
)- 不会发生死锁:即使有客户端在持锁期间由于崩溃而没有主动解锁,也能保证后续客户端加锁成功(主动释放;过期自动释放)
- 容错性:只要大部分的Redis节点正常运行,客户端就能正常加锁和解锁
- 解铃还须系铃人:加锁和解锁过程必须由同一个客户端执行,客户端A不能对客户端B所加的锁进行解锁(将value赋值为
requestId
,代表加锁客户端的唯一请求标识)
# Redisson
底层通过lua脚本实现所有操作的原子性
RLock
(可重入锁)RLock.lock(超时时间)
锁的自动续期
若不指定锁对象的超时时间,则使用默认超时时间
lockWatchdogTimeout
为30s;一旦占锁成功,启动定时任务每10s(internalLockLeaseTime
)执行一次续期脚本RReadWriteLock
(互斥锁/排他锁与共享锁)保证读操作能获取最新数据
- 并发读:同时获取读锁
- 并发写/并发读写(读-写或写-读):阻塞等待前一线程操作(占有读锁或写锁)成功
RSemaphore
限量操作(通过对锁对象值的增减进行控制)
实现分布式限流
// RSemaphore s; if (s.tryAcquire()) { // ... // s.release(); } else { return "busy"; }
RCountDownLatch
主从/集群架构故障转移时的锁失效问题
- Redlock多写
# 缓存一致性
通用策略:根据数据时效性合理设置缓存过期时间,使所有的读操作最终以数据库为准(最终一致性)
# 双写(弃用)
数据更新时先写数据库再覆盖缓存(数据查询时,操作步骤均为先读数据库再写入缓存)
由于双写操作非原子操作,可能导致先更新DB的线程后更新缓存,从而造成缓存不一致
注意事项:
- 双写过程加锁
# Cache Aside(后删)
数据更新时先写数据库再删缓存
注意事项:
- 后删过程加锁
- 若写场景较多,且写入缓存值需经过计算获取,则可使用Cache Aside模式
- 若第二次未能成功删除缓存导致缓存不一致:
- 将删除失败的缓存key或**MySQL binlog日志数据(借助Canal)**存至MQ以进行延时重试
# 延迟双删
public void write(String key, Object data){ redis.del(key); db.update(data); Thread.sleep(1000); redis.del(key); }
数据更新时先淘汰缓存后写数据库再延迟(~1s)删缓存
注意事项:
- 第一次删除缓存目的:❓
- 第二次延时删除缓存的目的:确保该时刻其他线程的写缓存操作已经结束,删除缓存后不会再有其他线程写入脏数据
- 主备分离场景下,延时时间需由主备同步延时时间决定
- 第二次删除缓存时开辟新线程进行异步操作,提高工作线程(写线程)吞吐量
- 同样存在Cache Aside模式中淘汰缓存失败所造成的缓存不一致问题
# 操作串行化
- 写操作:先删缓存,再将更新数据库的操作放入有序队列
- 读操作:若缓存未命中,读操作也放入有序队列
若读请求大量积压:
- 将任务队列水平拆分,提高并行度
- 使用适当的Hash一致性算法保证相同请求路由至相同队列中
- 合理配置限流与降级策略
缓存一致性场景分类:
- 低并发操作
- 基础数据(菜单分类等):时效容忍度高,可设置较长的缓存过期时间等待数据稳定;也可借助Canal订阅MySQL binlog方式实现缓存数据同步
- 用户纬度数据(订单数据、用户信息数据等):并发低,可设置合理的缓存过期时间等待数据稳定
- 高并发操作
# 删除策略与淘汰算法
# 删除策略
- 定时删除策略
- 惰性删除策略:获取数据时若发现该数据已过期则进行删除
- 定期删除策略:周期性(每隔100ms)随机抽查存储空间(随机、重点抽查)
可能导致缓存雪崩,需灵活配置不同数据的过期时间
# 淘汰/逐出算法
# 最大可使用内存(60%~70%)
> maxmemory
# 每次随机获取待删除数据的个数
> maxmemory-samples
# 待删除数据的逐出算法
> maxmemory-policy
# maxmemory-policy
- 检测易失数据
volatile-lru
volatile-lfu
volatile-ttl
volatile-random
- 检测全表数据
allkeys-lru
allkeys-lfu
allkeys-random
- 禁用
no-eviction
通过
INFO
信息中的keyspace-hits
和keyspace-misses
属性值判断当前Redis服务的性能,以选择合适的调优策略
# 高可用
# 主从
# 工作流程
建立连接
- slave保存master的ip与端口信息(
slaveof <ip> <port>
) - slave根据master信息创建连接master的socket,定时发送
ping
指令 - master保存slave的端口信息(
replconf listening-port <port>
)
- slave保存master的ip与端口信息(
数据同步
slave向master发送
psync2 ? -1
请求数据同步master执行
bgsave
尝试建立快照,同时建立存储增量数据的复制积压缓冲区master发送
+FULLRESYNC <runid> <offset>
,并将RDB文件通过socket发送给slaveslave接收RDB文件和runid以及offset信息,清空本地数据并基于RDB执行数据恢复
slave通过
psync2 <runid> <offset>
告知master数据同步完成(全量复制)(若runid与offset核验通过)master将复制缓冲区中指令(aof指令逐字符地排布)以及更新后的offset(
+CONTINUE offset
)发送给slave(若runid与offset核验不通过)重新开始全量复制
slave接收aof指令,保存新offset,并执行
bgrewriteaof
进行增量数据恢复slave告知master增量数据的运行id与offset,若master核验通过,则本次增量同步完(增量复制)
通过
repl-backlog-size
指令合理控制复制缓冲区大小,避免全量复制时间过长时复制缓冲区溢出导致的数据丢失(此时再次执行全量复制,产生死循环)最优复制缓冲区大小 = 2 * m-s平均重连时长 * m平均每秒写容量
通过
slave-serve-stale-data no
使slave端只提供有限的写数据功能(不对外提供数据服务)
命令传播
- master定时向slave进行心跳检测
- slave向master发送
REPLCONF ACK
响应
在
INFO REPLICATION
信息中的slave0等字段中维护距离上一个心跳包的时间间隔,正常值为1(或0)命令传播阶段的配置项
min-slaves-to-write <num>
min-slaves-max-lag <time>
成功响应心跳的slave个数小于num,或所有slave的延迟都大于time时,强制关闭master的写功能,停止数据同步
repl-timeout
若slave在给定时间没有响应,则master将其释放
repl-ping-slave-period
心跳检测时间间隔(一般为上一项超时时间的1/10~1/5)
# 哨兵
- 监控(同步信息)
- 连接master获取info
- 进行cmd连接以进行后续的命令交换
- 根据info信息连接关联的slave或sentinel
- 在同时存在的多个sentinel节点间组建发布订阅网络
- 通知
- 故障转移
- 发现问题
- 竞选负责人
- 负责人优选新master
- 切换各节点主从关系
# 集群
- Redis集群中设置16384个哈希槽,集群中的每个节点负责一部分hash slots
- 存储key时,使用该key的CRC16值对16384取模以获取目标存储槽位
- 节点间槽位可互相转移(并行转移),使得集群拓展性高
集群主从模式
Redis集群主从节点之间采用异步复制共享数据,因此不能保证强一致性,可能导致数据丢失
# 底层原理
# 线程模型
# File Event Handler(单Reactor单线程模型)
单线程内存操作:IO多路复用程序(采用单线程、非阻塞地)同时监听多个Socket,将读入的Socket事件放入队列中,由dispatcher依次分配对应的handler
- 多个Socket
- IO多路复用程序(事件分离器+IO处理器)
- 事件分派器
- 事件处理器
- 连接应答处理器
- 命令请求处理器
- 命令回复处理器
← MySQL基础