在没有引入缓存前,为了应对大量流量,一般采用:
这种方式下,随着访问量的增大,响应力越差,逐渐无法满足用户体验。
在引入缓存后:
旁路缓存模式:Cache Aside Pattern
DB
,然后直接删除 cache
cache
中读取数据,读取到直接返回,否则查 DB
后返回,然后将查到的数据写入 cache
读写穿透模式:Read/Write Through Pattern
cache
,cache
中不存在,直接更新 DB
,否则先更新 cache
,然后 cache
服务更新 DB
cache
中读取数据,读取到直接返回,否则查 DB
后写入到 cache
,之后返回数据异步缓存写入:Write Behind Pattern
cache
,然后异步批量更新 DB
cache
中读取数据,读取到直接返回,否则查 DB
后写入到 cache
,之后返回数据以上三种经典的读写策略在一定条件下都会产生缓存和数据库数据不一致的问题,这里给出两种解决方案
同步方案:
异步方案:
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。
因为,本地缓存只在当前服务里有效,部署两个相同的服务,他们两者之间的本地缓存数据无法共通。
常见的分布式缓存方案为:
共同点:
区别:
共同点:
区别:
Redis 常见的数据类型:字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Zset)
对于 Key 的设计,一般遵循如下规范:
数据库名:业务名:表名id
String 的底层结构是,简单动态字符串(Simple Dynamic String,SDS),特点:
在保存数字、小字符串时因为采用 INT 和 EMBSTR 编码,内存结构紧凑,只需要申请一次内存分配,效率更高,更节省内存。
对于超过44字节的大字符串时则需要采用 RAW 编码,申请额外的 SDS 空间,需要两次内存分配,效率较低,内存占用也较高,但大小不超过 512 MB,因此建议单个 value 尽量不要超过 44 字节。
缓存对象:
set key value
单值缓存针对数值进行操作:
# 设置数值数据增加指定范围的值
incr key
incrby key increment
incrbyfloat key increment
# 设置数值数据减少指定范围的值
decr key
decrby key increment
# 设置数据具有指定的生命周期
setex key seconds value
psetex key milliseconds value
分布式锁:setnx key:xxx true
设置分布式锁,用于请求限流
注意:
incr
,decr
时会转成数值型进行计算Long.MAX_VALUE
)主页高频访问信息显示控制,例如:新浪微博大 V 主页显示粉丝数与微博数量,需要针对这些高频访问的信息进行缓存处理
解决方案:
在 Redis 中为大 V 用户设定用户信息,以用户主键和属性值作为 Key,后台设定定时刷新策略即可
user:id:3506728370:fans 114514
user:id:3506728370:blogs 1919
user:id:3506728370:focuses 810
使用 json 格式保存数据
user:id:3506728370 {"fans":12210947, "blogs":6164, "focuses":83 }
与 Java 中的 HashMap 类似,但底层结构是压缩列表和哈希表,特点:
存储形式为:key-value
,其中 value=[{field1, value1}, {field2, value2}, {field3, value3}]
,如下图所示:
与 Java 中的 HashMap 不同的是,Redis 中的 Hash 底层采用了渐进式 rehash 的算法,在做 rehash 时会创建一个新的 HashTable,每次操作元素时移动一部分数据,直到所有数据迁移完成,再用新的 HashTable 来代替旧的,避免了因为 rehash 导致的阻塞,因此性能更高。
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了:
Listpack
的内部结构通常由一个连续的字节数组组成,其中包含了列表的元素和元数据对象缓存:
HMSET user {userId}:username zhangfei {userId}:password 123456
HMSET user 1:username zhangfei 1:password 123456
HMGET user 1:username 1:password
针对数值进行操作:
# 设置指定字段的数值数据增加指定范围的值
hincrby key field increment
hincrbyfloat key field increment
注意:
nil
双十一期间,电商平台用户购物车信息存储,用户会对购物车信息进行频繁访问和修改
解决方案:
购物车信息存储:
{field1, value1}
,其中 field1
商品 id
,value1
为数量,也可以选择型将价格、活动信息添加购物车操作:
HSET cart:{userId} {商品Id} 1
HINCRBY cart{userId} {商品Id} 1
HLEN cart:{userId}
HDEL cart:{userId} {商品Id}
HGETALL cart:{userId}
当前仅仅是将商品 id
存储到了 Redis 中,在回显商品具体信息的时候,还需要拿着商品 id
查询一次数据库,获取完整的商品的信息
List 其实就是链表,只不过在 Redis 的实现中是双向的,如图所示:
3.2 之前的版本 List 底层由由双向链表和压缩列表实现,特点:
3.2 之后的版本,底层仍采用了 ZipList(压缩列表)来做基础存储。当压缩列表数据达到阈值则会创建新的压缩列表。每个压缩列表作为一个双端链表的一个节点, 终形成一个 QuickList 结构。
QuickList:
ziplist
组成,其中每个 ziplist
都是一个紧凑的压缩列表(compressed list),用于存储有序列表的元素。每个 ziplist
都包含多个节点,每个节点都可以存储一个元素。ziplist
都有一个 level 属性,表示该 ziplist
中节点的高度。通过在不同层级的 ziplist
上进行跳跃,可以快速定位到目标范围的起始位置,并进行后续的线性搜索。移除指定数据:
lrem key count value
规定时间内获取并移除数据:
blpop key1 [key2] timeout
brpop key1 [key2] timeout
brpoplpush source destination timeout
注意:
微信公众号发布文章或视频平台关注的博主发动态,在关注列表里面,这些消息要求按照时间进行推送
解决方案:
LPUSH
或 RPUSH
的方式压入队列中LPUSH msg:{userId} xxx
LRANGE msg:{userId} 0 4
Set 类型的底层数据结构是由哈希表或整数集合实现的:
Set 与 Hash 存储结构完全相同,但 Set 仅存储键,不存储值(nil),并且值是不允许重复的
Set 最具特色的就是集合运算:
求两个集合的交、并、差集:
sinter key1 [key2 ...]
sunion key1 [key2 ...]
sdiff key1 [key2 ...]
求两个集合的交、并、差集并存储到指定集合中:
sinterstore destination key1 [key2 ...]
sunionstore destination key1 [key2 ...]
sdiffstore destination key1 [key2 ...]
将指定数据从原始集合中移动到目标集合中:
smove source destination member
在需要获取用户共同关注的场景下,利用 Set 的集合运算再合适不过了
咨询和论坛交流类网站通常针对用户有严格的约束,因此有对黑名单和白名单功能的需求
解决方案:
对于某个平台需要举办抽奖活动,保证参与的账号唯一且不能重复中奖
解决方案:
SADD key {userId}
SRANDMEMBER key [n] / SPOP key [n]
SortedSet,也叫 ZSet:
对于跳表(SkipList)首先是链表,但与传统的链表相比有几点差异:
微博热搜排行榜、直播打赏排行榜、视频热门排行
解决方案:
ZINCRBY hot:news 1 title1
ZREVRANGE hot:news 0 9 WITHSCORES
BitMap 即位图,是一串连续的二进制数组
BitMap 内部存储形式如图:
存储对比:
基础操作:
SETBIT
:为位数组指定偏移量上的二进制位设置值,偏移量从 0 开始计数,二进制位的值只能为 0 或 1。返回原位置值。GETBIT
:获取指定偏移量上二进制位的值。BITCOUNT
:统计位数组中值为 1 的二进制位数量。BITOP
:对多个位数组进行按位与、或、异或运算。签到统计,统计登录用户
解决方案:
SETBIT uid:online:202403 15 1
设置 uid 的用户在 2024 年 3 月的 16 日进行了签到GETBIT uid:oneline:202403 15
返回 1 说明 uid 用户在 2024 年 3 月的 16 日进行了签到BITCOUNT uid:oneline:202303
Redis3.2 中增加了对 GEO 类型的支持。GEO,Geographic,地理信息的缩写
基础操作:
GEOADD location-set longitude latitude name [longitude latitude name...]
GEOPOS location-set name [name ...]
GEODIST location-set location-x location-y [unit]
unit
可选参数为 m | km | mi | ft
分别代表返回值的单位为米、千米、英里、英尺,不添加则默认单位为米GEODIST hubeiCities wuhan yichang
GEORADIUS location-set longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [ASC|DESC] [COUNT count]
radius
半径大小,可选单位米、千米、英里、英尺WITHCOORD
:可选参数,添加则在返回匹配的位置时会将该位置的经纬度一并返回WITHDIST
:可选参数,添加则在返回匹配的位置时会将该位置与中心点之间的距离一并返回ASC|DESC
:可选参数,添加 ASC
将返回的匹配位置根据距离从近到远排序,DESC
则相反COUNT
:可选参数,限制结果数量GEORADIUSBYMEMBER location-set location radius m|km|ft|mi [WITHCOORD] [WITHDIST] [ASC|DESC] [COUNT count]
GEORADIUS
用法一致存储不同位置信息:
hubeiCities
集合中:GEOADD hubeiCities 114.32538 30.534535 wuhan
GEOADD hubeiCities 112.161882 32.064505 xiangyang 111.305197 30.708127 yichang 111.583717 30.463363 zhijiang
获取位置信息:
GEOPOS hubeiCities wuhan
结果返回经度和纬度信息GEOPOS hubeiCities xiangyang yichang zhijiang
指定范围查询位置信息:
hubeiCities
集合中 112.927076 28.235653 (长沙) 500km 以内的位置信息,查找结果中应包含不超过 5 个位置的坐标信息,距离信息,并按距离由近到远排序:GEORADIUS hubeiCities 112.927076 28.235653 500 km withcoord withdist asc count 5
hubeiCities
位置集合中查找距离武汉200km 以内的位置信息(这里指定的目标位置只能是 hubeiCities
中存在的位置,而不能指定位置坐标),查找结果中应包含不超过 2 个位置的坐标信息,距离信息,并按距离由远到近排序:GEORADIUSBYMEMBER hubeiCities wuhan 200 km withcoord withdist desc count 2
Redis 提供了发布订阅功能,可以用于消息的传输。
Redis的发布订阅机制包括三个部分:
publisher:
PUBLISH
命令将消息发送到一个特定的频道subscriber:
SUBSCRIBE
命令订阅自己感兴趣的频道channel:
SUBSCRIBE channel1 channel2
,Redis 客户端 channel1
订阅 客户端 channel2
PUBLISH channel message
,Redis 客户端 channel
发布一条 message
,订阅了该 channel
的客户端将收到 message
UNSUBSCRIBE channel
,退订 channel
,不再接收来自 channel
的消息PSUBSCRIBE ch*
,根据正则表达式匹配订阅,订阅所有以 ch
开头的 channel
PUNSUBSCRIBE ch*
,根据正则表达式匹配退订,退订所有以 ch
开头的 channel
在 Redis 哨兵模式中,哨兵通过发布与订阅的方式与 Redis 主服务器和 Redis 从服务器进行通信
Redisson是一个分布式锁框架,在 Redisson 分布式锁释放的时候,是使用发布与订阅的方式通知的
注意:如果是注重业务的消息,推荐用消息队列实现
Redis事务的本质是一组命令的集合:
注意:Redis 的事务远远弱于 mysql,严格意义上,它不能叫做事务,只是一个命令打包的批处理,不能保障失败回滚
事务的创建机制:
multi
命令,实际上会开启一个命令队列,后续的命令将被视为事务操作添加到该命令队列exec
命令,则批量提交队列中的命令,事务完成discard
,则不执行命令,直接清空队列事务的回滚机制:
watch监听机制;
watch
命令可以通过监控某个 key 的变动,来决定是不是回滚Iua 是一种轻量小巧的脚本语言,用标准 c 语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活 的扩展和定制功能。
由于 Redis 回滚机制并不完善,因此用 Redis 的事务一般引入 LUA 脚本来实现:
自 2.6.0 起可用,通过内置的 lua 编译/解释器,可以使用 EVAL 命令对 lua 脚本进行求值:
EVAL script numkeys key [key ...] arg [arg ...]
script
:该参数是一段 lua 5.1 脚本程序,脚本不必(也不应该)定义为一个 lua 函数numkeys
:用于指定键名参数的个数key
:需要操作的键,可以指定多个,在 lua 脚本中通过 KEYS[1]
、KEYS[2]
获取arg
:附件的参数,可以指定多个,在 lua 脚本中通过 ARGS[1]
、ARGS[2]
获取redis.call()
: redis.call('SET', 'KEY:A', '114514')
redis.pcall()
: redis.pcall('GET', 'KEY:A')
注意:脚本中,使用 return 语句将返回值返回给客户端,如果没有 return,则返回 nil
注意:
慢查询配置相关的参数
slowlog-log-slower-than
: slowlog-log-slower-than 100
,执行时间超过100微秒的命令就会被记录到慢查询日志slowlog-log-max-len
: slowlog-log-max-len
值时,服务器在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除上述配置,在 Redis 中有两种修改方法:
config set
命令动态修改:config set slowlog-log-slower-than 100
查看慢查询日志:
slowlog get
:查看慢查询日志,slowlog get 10
查看最新的 10 条慢查询记录1) 1) 3 // 表示这是第三个被记录的慢查询
2) 1710583725 // Unix 时间戳,表示该慢查询发生的具体时间
3) 5 // 代表查询执行的时长,单位为 ms
4) 1) "set" // 具体的 Redis 命令
2) "b" // 命令的 参数
3) "a"
5) "127.0.0.1:2474" // 发出此指令的客户端的 IP 地址和端口号
6) "" // 代表该查询所在的数据库 ID,"" 表示默认数据库