05-Redis

9 阅读8分钟

一、数据类型

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

  1. 首先从第2层一直找到第0层插入的位置,分别是1,4,5
  2. 抽签决定6出现在第几层,如果抽到第2层,则该层到第0层都需要插入6
第 2 层: 1 --------6---------> 7
第 1 层: 1 ----> 4 ----6----> 7
第 0 层: 1 -> 2 -> 4 -> 5 -> 6 -> 7
  1. 如果抽到第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. 初始化

格子01234567
值00000000

2. 商品1创建

商品1通过哈希计算命中格子1和4

格子01234567
值01001000

3. 商品2创建

商品2通过哈希计算命中格子4和6

格子01234567
值01001010

4. 查询

  1. 商品3在查询时,先走redis的布隆过滤器,命中2和3,发现1个0,直接判断它不存在,直接走mysql查询。

  2. 商品4在查询时,命中1和6,发现都是1,此时说明可能存在,先走redis查询,如果没有再走mysql查询。

四、过期淘汰

4.1 过期

redis中存储的数据几乎都会设置有效时间,时间到期后,redis不会全量的扫描所有的数据去清理数据,通常是如下两种

  1. 下次有人来 GET 这个 key,发现过期了,当场删掉,返回空。一直没人读,它就先留在内存里。
  2. 隔一小段时间,从设了过期的 key 里抽一批来查,到期的删掉。

所以,如果一大批key在同一个时间点过期时,会导致请求一起打到数据库上,这种行为称为雪崩。因此过期时间要加一点随机数,让它们的过期时间错开。

4.2 内存淘汰

redis把数据存储到内存,但如果超过了内存上限,会触发淘汰机制。

1.noeviction

默认策略,内存超出后,不会删除已有的数据,后续写入的直接报错

2.allkeys-lru

所有 key 里,扔掉最久没被访问的,最常见的策略。它并不是把所有的数据key列一个清单,标记它们多久没被使用了,而是随机的抽查一部分key,丢掉最近没有被使用的key。

4.3 redis内存溢出排查

  1. 判断内存是否满+溢出策略是什么
  2. 查找大key并分析内存占用
redis-cli --bigkeys
redis-cli MEMORY USAGE user:3

3.TTL 是 -1,说明没设过期,会一直占着。大 key 不要了就 UNLINK,不要用 DEL。如果是很多小key没有设置过期,则补充过期时间,策略调整成allkeys-lru。

五、缓存三大问题

5.1 穿透

用户使用随机的id去查询不存在的数据,导致请求一直查询数据库。

解决方案

  1. 空值缓存:请求的id如果redis没有,数据库也不存在时,添加redis的空值缓存30s,后续相同的id在短时间请求时,直接输出空缓存即可

  2. 布隆过滤器:适用于高并发场景,如果过滤器查询是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是单线程)。

解决方案

  1. 大key可以进行拆分,例如大字符串可以拆成约定的多个字符串,大list拆分成多个list,当然也可以拆分成hash数据,比较灵活。
  2. 删除大key用unlink,而不是del直接删除,unlink适用于所有数据的删除,它是让key失效,等一会后台线程再去删除。

6.2 热点key

热点key问题指,这个key访问量过大,所有的请求都访问这个key,导致redis服务器很容易崩。

解决方案

  1. 在本地短期缓存热点key,这样大部分请求都不会走redis
  2. 设置集群,把热点key拆分成多个key1,key2,然后每个机器承担一部分压力即可

七、缓存一致性

  1. 数据读取时,先走缓存,缓存没有,再走数据库,然后再添加缓存。
  2. 数据变更时,先删除缓存,删除失败,就把这个任务放到消息队列,不能阻塞用户操作,等待消息队列删除。