一、为什么用Redis?
一开始,Web访问服务端,服务端访问Mysql获取数据。随着时间的发展,一个数据库支撑不下去了,发展成了集群:
随着访问量的增加,有些数据是热点数据,会被经常访问到,因此,如何快速访问到热数据是一个关键问题。
在这个背景下,Redis出现了,Redis是一种基于内存框架,将热数据存在Redis中等价于存在内存中。
那么会有人问了,在内存中,电脑重启了数据不就是没了吗?
是这样的,基于此,Redis提供了持久化操作:
- 全量数据RDB文件
- 增量数据AOF文件
全量RDB文件是对数据当前状态保存一个快照,是二进制文件。
增量数据AOF是记录对Redis的操作至一个文件中。
启动的时候(开启AOF,RDB混合模式),先从RDB中加载全量文件,再根据AOF加载增量的数据文件,完成数据恢复。
最重要的一点是,Redis是单线程处理所有的操作命令:
二、数据结构详解
01、数据结构:
考虑三点设计:
- 足够节省空间
- 能够很快的读出数据
- 能够很快的修改数据,也就是写进去。
在Redis中,String与其他语言定义的不同,底层实现是,
格式如下:
- len:buf处实际占用大小
- alloc:buf处分配的空间大小
- flags:标志
- buf:实际存储数据的位置
首先会在指针处左移获取
alloc, len信息,然后右移读取具体的数据。
02、数据结构
数据结构,由双向链表和
实现:
双向链表处有一个entry字段,该字段对应的是listpack数据结构,为什么要用这个呢? 主要是为了更多的存储数据。 下面仔细看一下listpack结构:
由于element是变长的,因此,我们需要知道起始位置,还需要知道数据类型。因此,element的数据格式如下:
03、
Hash和大部分一样,但是有一点,扩槽的时候如何做?
Rehash:直接将中所有数据全部迁移到
中,数据量大的时候会阻塞用户请求。
- 渐进式
Rehash:每次用户访问时都会迁移少量数据,将整个迁移过程,平摊到所有访问用户的请求过程中。
04、:
首先看一下skiplist(跳跃表):
假设我们要找,先在顶层的
Head节点出发,走到发现后续没节点了,到下一层。
在处,发现
比
大,因此向右走,走到
,然后没节点了,进入下一层。
在处,发现
比
小,因此向左走,正好找了
。时间复杂度
。
zskiplist + hash:
三、实用案例
1、连续签到
用户每日有一次签到的机会,如果断签,连续签到的技术将会归0.
连续签到的定义:每天必须在前签到。
Key: 用户唯一id
Value: 连续签到次数
expireAt: 过期时间,默认应该是后天0点。
用到了数据结构
2、消息通知:
用list作为消息队列:
使用场景:消息通知
例如,当文章更新的时候,将更新后的文章推送到ES,用户就能搜到最新的文章数据。
3、计数
一个用户有多项计数需求,可以通过hash结构存储
例如,个人主页统计这些数据,如果存在mysql中,访问量很大的话,对Mysql的负荷很大。
因此可以用Redis中的Hash表。
4、排行榜
积分变化时,排名要实时变更:zset
5、限流
- 要求
秒内放行的请求为
,超过
则禁止访问。
- 数据结构:
String
Key: comment_freq_limit_$timeStamp$
对上面的Key调用incr,超过限制则禁止访问。
6、分布式锁:
使用setnx命令,利用两个特性:
- Redis是单线程执行命令。
setnx只有未设置过的才能执行成功。
SetNX不是高可用的分布式锁实现,还可能有以下问题:
- 某个占用锁的业务超时了,没办法释放分布式锁,导致其他线程没办法获取锁。
- 主从切换的时候,主节点还没有将分布式锁的状态同步到新主节点,这时候有人在新节点请求分布式锁,引起并发安全问题。
- Redis集群脑裂,导致出现多个主节点。
四、Redis使用注意事项
01、大Key、热Key
| 数据类型 | 大key标准 |
|---|---|
| String | value的字节数大于10KB |
| Hash/Set/ZSet/list等 | 元素个数大于5000个或总value字节数大于10MB |
大Key的危害:
- 读取成本高
- 容易导致慢查询(过期、删除)
- 主从复制异常导致读操作不可用,服务阻塞,无法正常响应请求
业务侧使用大key的表现
- 请求Redis超时
消除大key的方法:
String
- 拆分key:
- 压缩
将value压缩后写入Redis,读取时解压后再使用,一般使用gzip、snappy、lz4等。具体选择哪一个,需要测试一下哪个效果好。
如果存储的时JSON字符串,可以考虑使用MessagePack进行序列化。
集合类hash、list、set、zset:
- 拆分:用Hash取余+ 位掩码的方式决定放在哪个key中
- 区分冷热:例如,榜单列表场景使用zset,只缓存前10页数据,后续走database
热Key:用户访问一个Key的QPS特别高,导致Server实例出现CPU负载突增或者不均。
解决策略:
- LocalCache:将热key存在用户端,降低访问
Redis的QPS,如果LocalCache中缓存没有命中,或者过期了,从Redis中将数据更新到LocalCache中。Java中的Guava、Golang的BigCache就是Localcache。 - 拆分:将热key复制写入多份,例如:
key1:value; key2:value,访问的时候随机key几。缺点就是,更新的时候需要更新多份,一旦有一个没有更新上,就会导致不一致。 - 使用Redis代理的热Key承载能力:
localcache挺好,但是每个服务都要配置一个localCache,业务多了,这个难免也多了。字节跳动内部使用Redis访问代理,本质上结合了热Key发现,localCache两个功能。
02、慢查询
- 批量操作,一次性传入过多的
key/value,如mset, hmset等,即便是有pipeLine,建议单批次不要超过。
zset命令大部分都是的,当大小超过
的时候,也可能导致慢查询。
- 操作大key
03、缓存穿透、缓存雪崩
- 缓存穿透:访问的数据Redis不存在,直接查找数据库。
- 缓存雪崩:大量缓存同时过期。
解决缓存穿透:
- 对于不存在的key,在redis中缓存一个空值。
- 布隆过滤器。
缓存雪崩:缓存存两份,差异化两个缓存过期时间。