一、数据类型
1.1 String
新增、更新
set key value
删除
del key
查询
get key
自增:redis的string类型兼容number,如果存储的是number类型,可以实现INCR自增操作
set num 1
INCR num //2
新增+缓存时间
setex key 60 value //表示60s后过期
1.2 Hash
hash类型适用于存储整份对象的,例如商品属性,用户资料等。
新增
hset name key1 value1 key2 value2
读
读取对象的某个属性
hget name key
读取完整对象
HGETALL name
删除
hdeal name key1 key2
1.3 List
数组类型,按顺序排列的字符串数组,常用于消息队列和最新几条动态获取。它的左边是头,右边是尾巴。
增
list既支持头插,又支持尾插。
lpush name value //头插
rpush name value //尾插
查询
lrange name index1 index2 //访问index1-index2范围内的数据
删除
lpop name //从左边弹出,头删
rpop name //从右边弹出,尾删
1.4 Set
无序的存储多个字符串数据,并且数据不能重复。
增
sadd name value
查询
SMEMBERS name // 获取所有数据
scard name //统计数据个数
删除
srem name value
是否存在
SISMEMBER name value // 1存在,0不存在
1.5 ZSet
Zset在set基础上增加了分数,支持了自动排序
增
zadd name score value
更新
ZINCRBY name score value //在原来基础上增加15分
查询
ZREVRANGE name 0 -1 WITHSCORES // 从高到低列出所有人,带分数
ZREVRANK name value //查询某个值的排名
ZSCORE name value //查询某个值的分数
删除
zrem name value
二、跳表
跳表是ZSet获取排名范围的数据结构。
2.1 结构
跳表的示例如下所示,第0层存储所有数据的分数,按照从小到大顺序排列,数据结构是双向链表,支持从前到后,也支持从后到前查询。从上往下看,除了第0层其它都是是索引层,它们的结构是单向链表。
第 2 层: 1 -----------------> 7
第 1 层: 1 -------> 4 ------> 7
第 0 层: 1 -> 2 -> 4 -> 5 -> 7
2.2 查询
所有层中分数相同的节点地址是同一个,所以支持直接跳转查询。如果某层找到数据后,无需下一层查找。
案例1:查询5
从顶层第2层开始,1<5<7;来到第1层,继续从1开始遍历,发现4<5<7;来到第0层,直接从4开始往后遍历发现了分数5。
案例2:查询4
从顶层第2层开始,1<4<7;来到第1层,继续从1开始遍历,发现4成功返回。
2.3 新增
第 2 层: 1 -----------------> 7
第 1 层: 1 -------> 4 ------> 7
第 0 层: 1 -> 2 -> 4 -> 5 -> 7
案例1:插入数据6
- 首先从第2层一直找到第0层插入的位置,分别是1,4,5
- 抽签决定6出现在第几层,如果抽到第2层,则该层到第0层都需要插入6
第 2 层: 1 --------6---------> 7
第 1 层: 1 ----> 4 ----6----> 7
第 0 层: 1 -> 2 -> 4 -> 5 -> 6 -> 7
- 如果抽到第0层,按照规则,它已经是最底层,只需插入到当前层即可。
第 2 层: 1 -----------------> 7
第 1 层: 1 -------> 4 ------> 7
第 0 层: 1 -> 2 -> 4 -> 5 -> 6 -> 7
2.4 ZSet
zset有两个数据结构,1个存储数据成员到分数的映射表,另一个就是跳表,专门存储分数。需要获取某个数据的分数时,直接找映射表获取,如果找数据的排名,或者区间范围,则需要走跳表。
三、布隆过滤器
布隆过滤器用来判断是否存在某个事物,如果判断不存在,则一定不存在,如果存在,则事物是可能存在。常用于缓存穿透的解决。
3.1 原理介绍
它在redis中是一个个排列的格子,每个格子有自己的id,格子里面的值初始化全是0。一个商品在创建完成后通过哈希算法计算它所属的格子,然后给格子的赋值为1,所有的商品共用同一份key。
1. 初始化
| 格子 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2. 商品1创建
商品1通过哈希计算命中格子1和4
| 格子 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 值 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
3. 商品2创建
商品2通过哈希计算命中格子4和6
| 格子 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 值 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 |
4. 查询
-
商品3在查询时,先走redis的布隆过滤器,命中2和3,发现1个0,直接判断它不存在,直接走mysql查询。
-
商品4在查询时,命中1和6,发现都是1,此时说明可能存在,先走redis查询,如果没有再走mysql查询。
四、过期淘汰
4.1 过期
redis中存储的数据几乎都会设置有效时间,时间到期后,redis不会全量的扫描所有的数据去清理数据,通常是如下两种
- 下次有人来
GET这个 key,发现过期了,当场删掉,返回空。一直没人读,它就先留在内存里。 - 隔一小段时间,从设了过期的 key 里抽一批来查,到期的删掉。
所以,如果一大批key在同一个时间点过期时,会导致请求一起打到数据库上,这种行为称为雪崩。因此过期时间要加一点随机数,让它们的过期时间错开。
4.2 内存淘汰
redis把数据存储到内存,但如果超过了内存上限,会触发淘汰机制。
1.noeviction
默认策略,内存超出后,不会删除已有的数据,后续写入的直接报错
2.allkeys-lru
所有 key 里,扔掉最久没被访问的,最常见的策略。它并不是把所有的数据key列一个清单,标记它们多久没被使用了,而是随机的抽查一部分key,丢掉最近没有被使用的key。
4.3 redis内存溢出排查
- 判断内存是否满+溢出策略是什么
- 查找大key并分析内存占用
redis-cli --bigkeys
redis-cli MEMORY USAGE user:3
3.TTL 是 -1,说明没设过期,会一直占着。大 key 不要了就 UNLINK,不要用 DEL。如果是很多小key没有设置过期,则补充过期时间,策略调整成allkeys-lru。
五、缓存三大问题
5.1 穿透
用户使用随机的id去查询不存在的数据,导致请求一直查询数据库。
解决方案
-
空值缓存:请求的id如果redis没有,数据库也不存在时,添加redis的空值缓存30s,后续相同的id在短时间请求时,直接输出空缓存即可
-
布隆过滤器:适用于高并发场景,如果过滤器查询是0,直接返回空,如果是1,继续查询数据库,如果数据库没有,则添加redis的30s空值缓存。
5.2 击穿
热点key在过期的瞬间,大批量的请求打到数据库,然后又一起重建缓存。
解决方案
加锁重建:所有的请求到redis时,发现没有缓存,只有1个线程能抢到锁,然后去mysql中读取到数据,写回缓存。其它线程在java中设置休眠时间等待,后面再去读取redis的缓存即可
SET lock:product:1 1 NX EX 10
5.3 雪崩
大批量的key在同一时间瞬间失效,导致大量的请求打到数据库上。
解决方案
所有的key过期时间添加一段随机数,例如原始是60s过期,可以加1-10s的随机数。如果是redis服务挂了,则需要备份主从redis机器,主机器挂了,从机器作为备用的缓存。
六、大key与热点key
6.1 大key
大key是指数据在redis中值太大,例如字符串太长,list数据存储数据量过大,一次读取或者删除都会阻塞redis(redis是单线程)。
解决方案
- 大key可以进行拆分,例如大字符串可以拆成约定的多个字符串,大list拆分成多个list,当然也可以拆分成hash数据,比较灵活。
- 删除大key用unlink,而不是del直接删除,unlink适用于所有数据的删除,它是让key失效,等一会后台线程再去删除。
6.2 热点key
热点key问题指,这个key访问量过大,所有的请求都访问这个key,导致redis服务器很容易崩。
解决方案
- 在本地短期缓存热点key,这样大部分请求都不会走redis
- 设置集群,把热点key拆分成多个key1,key2,然后每个机器承担一部分压力即可
七、缓存一致性
- 数据读取时,先走缓存,缓存没有,再走数据库,然后再添加缓存。
- 数据变更时,先删除缓存,删除失败,就把这个任务放到消息队列,不能阻塞用户操作,等待消息队列删除。