redis内存模型
c语言编写,K-V形式存储,K是String类型的,V支持5种不同的数据类型,分别是:string,list,hash,set,sorted set;但不管哪种类型value都是放在redisObject中,只是内部数据结构不一样。
底层数据结构有:简单动态字符串(SDS),链表,字典,跳跃表,整数集合,压缩列表。
底层数据模型
更详细的数据底层模型
rehash过程
redis基本数据类型和高级功能
String
使用场景:
- 计数器,如点赞,阅读量,资金方-> 资产总值
- 分布式主键
底层模型: SDS
Hash
使用场景:
- 存储对象 如资金方->账户
- 淘宝购物车
底层模型:
压缩列表(ziplist):哈希中元素数量小于512个;哈希中所有键值对的键和值字符串长度都小于64字节
哈希表(hashtable):不满足压缩列表时。另外redis的最外层K-V哈希,只有这种结构。
List
使用场景:
- 集合对象 资金方->资产id列表
- 订阅消息
底层模型:
压缩列表(ziplist):列表中元素数量小于512个;列表中所有字符串对象都不足64字节。
双端链表(linkedlist):不满足压缩列表时。
Set
使用场景
- 关注的人,共同好友,点赞的人
- 抽奖
底层模型:
整数集合(intset):集合中元素数量小于512个;集合中所有元素都是整数值。
哈希表(hashtable):不满足整数集合时。
SortedSet
使用场景:
- 排行榜
- 热度排序
底层模型:
压缩列表(ziplist):有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。
跳跃表(skiplist):不满足压缩列表时。
redis为什么选择跳表而不选择红黑树?
- 在做范围查找的时候,平衡树比skiplist操作要复杂。
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- 跳表的算法实现简单
bitmap
常用于数据统计,二值统计 segmentfault.com/a/119000004…
HyperLogLog
用于基数基数,在应用系统的开发中,我们常常会有类似这样的需求:统计某个网站的UV、用户搜索网站关键词的数量等等。我们可以使用基数计数来做这个功能。基数计数通常用来统计一个集合中不重复的元素个数。
HyperLogLog其实是一种概率型数据算法,非redis独有,redis中实现的HyperLogLog,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。
但是,因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
命令:
PFADD key value[value]
PFCOUNT key
地理位置GEO
发布订阅
bloom filter
zhuanlan.zhihu.com/p/140545941
redis高可用
redis持久化
RDB:redis定时将所有数据生成快照存在rdb文件中,所以rdb只能保存某一个时刻的数据,并不一定是最新的。也可以手动执行命令保存,save和bgsave,save会阻塞进程,一般用bgsave,bgsave会fork一个子进程去生成出来专门用于保存至rdb。rdb的优点是数据恢复比AOF快,文件大小也比AOF小。
AOF:将每一条更新命令保存到AOF日志文件中,类似于mysql的binlog,也是通过一个后台线程1秒一次去执行fsync命令进行日志写入。相对RDB数据更加实时,如果开启了该功能,则启动时默认使用AOF加载。但是开启后会比RDB更影响性能,因为1s一次写入嘛
redis主从同步
你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命令都发给slave。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。
redis sentinal哨兵模式
首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。 这里的哨兵有两个作用
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
实际过程中一般会有多哨兵,原理是一样的,就是增加一个投票的过程。多哨兵选举流程见选举文章
redis cluster集群
当单台redis服务器容量不够的时候就要考虑上集群了。多master多slave,横向扩展,负载均衡由redis内部模块redis-cluster负责,每个redis实例负责不同槽位的数据。
槽位分配算法就是类似于分库分表的分片算法。比如直接用hash(key)% n,对节点数量hash取模;复杂一点的呢就用一致性hash。
但是Redis集群(Cluster)并没有选用上面一致性哈希,而是采用了哈希槽(SLOT)的这种概念。
首先哈希槽其实是两个概念,第一个是哈希算法。Redis Cluster的hash算法不是简单的hash(),而是crc16算法,一种校验算法。
另外一个就是槽位的概念,空间分配的规则。其实哈希槽的本质和一致性哈希算法非常相似,不同点就是对于哈希空间的定义。一致性哈希的空间是一个圆环,节点分布是基于圆环的,无法很好的控制数据分布。而Redis Cluster的槽位空间是自定义分配的,类似于Windows盘分区的概念。这种分区是可以自定义大小,自定义位置的。
Redis Cluster包含了16384个哈希槽,每个Key通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。
内存淘汰机制
Redis的过期策略,是有定期删除+惰性删除两种。
惰性:每次访问key时检查是否过期;
定期:默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。
如果定期和惰性都没有删除到的key怎么办呢?这时候内存淘汰机制派上用场了。
注意:只有当内存使用量达到阈值了才会启用内存淘汰机制。
redis提供了多种策略可供选择:
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外),默认模式
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。推荐模式
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
LRU(least recently used)最近最少使用的淘汰,其实就是按照访问顺序排序,删除最老的。java有现成的实现linkedHashMap
面试题
1. 缓存雪崩,缓存穿透,缓存击穿?
- 缓存雪崩--同一时间大量key到期,会直接打到mysql,也会造成消耗大量的cpu资源
解决方案:
- 给过期时间加一个随机值
- mysql proxy限流
- 加分布式锁(严重影响性能)
- 主备(缓存备份或者叫拷贝)过期时间不一致,又叫二级缓存。
-
缓存穿透 --很多无效请求会打到db
1.查询mysql发现没有匹配结果时在redis中也创建key返回空值,就是对空值也进行缓存,并且这个缓存的有效时间要比较短比如1min。
2.bloom filter -
缓存击穿--热点key失效的一瞬间,大量请求打到db
-
"永远不过期": 这里的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
-
使用互斥锁(mutex key):这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了
-
限流,当缓存失效时访问db,对db的访问qps做限流控制,防止db被打挂
-
热点key进行二级缓存,先访问内存,再访问redis(但是缓存失效的时候二级缓存也是失效的,只能解决redis扛不住的热点key问题,不能解决击穿问题)
-
资源保护:可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
blog.csdn.net/u012240455/…
2. 双写一致性(重点)
一般使用方案
(1)读的时候先从redis读,没有就从db读,读完后再将数据set到redis中;
(2)写的时候先写db,再删redis,等下次读的时候redis才会加载这个数据;
为什么写完db不直接写redis呢?
假设有两个请求过来,是写同一个数据,那先写db的却后写redis,那这样redis存的是脏数据。还有一点就是,这样会每次写入都去更新缓存,太频繁了,可能还没用到这个缓存呢就更新了,浪费性能。
这种方案就能完全避免数据的一致性吗?
其实这种方案也会有并发问题,就是概率很小
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
针对这个问题,我们可以采取如下方案:
1.加锁:更新的时候先去拿分布式锁,然后等所有操作成功了再释放锁,也就是同时只能一个线程进行更新操作,但是这样增加了成本,牺牲了性能,实际使用redis就是为了提高性能和并发能力,结果加了锁并发能力下降很多,得不偿失,有强一致性需求的场景不应该引入缓存
2.延时双删:先删除缓存,再更新数据库,sleep几百ms,再删除缓存,但是这样吞吐量会降低,推荐
3.将有效时间设置的短一点,比如30s,这种方案治标不治本,无法保证强一致性
4.订阅mysql的binlog来更新redis,不可取;
redis 为什么这么快?
- 纯内存操作
- 单线程+多路复用
- 数据结构简单,有些优化
怎么发现热点key
(1)热点检测
① 凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此Key作为热点Key
② 客户端收集:在操作Redis之前对数据进行统计
③ 抓包进行评估:Redis使用TCP协议与客户端进行通信,通信协议采用的是RESP,所以能进行拦截包进行解析
④ 在proxy层,对每一个 redis 请求进行收集上报
⑤ Redis自带命令查询:Redis4.0.4版本提供了redis-cli –hotkeys就能找出热点Key
如果要用Redis自带命令查询时,要注意需要先把内存逐出策略设置为allkeys-lru或者volatile-lru,否则会返回错误。进入Redis中使用config set maxmemory-policy allkeys-lru即可。
热点key问题
多级缓存方案 tech.youzan.com/tmc/
redis分布式锁
set nx ex实现
此实现没有可重入性,需要可重入,由redission通过lua实现,采用hash的数据结构,value存储重入次数
怎么保证锁的一致性
当master写入成功后宕机,slave并没有这个锁,导致切主后该锁被其他线程持有。解决方案:redlock,集群中大部分实例成功才算成功。
怎么保证正确的释放锁
在业务时间内未释放锁,锁失效,第二个线程加锁成功,第一个线程恢复,第一个线程释放锁的时候会把第二个线程的锁给释放了。解决方案:锁的value中加入唯一标识,如线程id,uuid等,释放时先check锁是自己的,再释放,这两步用lua脚本实现
怎么保证业务时间内正常释放锁
可以使用watchdog无限续期,watchdog就是另起一个定时任务,每1/3时间去查询该实例持有的锁是否被释放,没有被释放则续期,只有当业务实例不可用这个锁才能被别的实例持有,在锁释放时也同时会删除watchdog任务,即使释放失败也会删除。
redission怎么实现超时等待获取锁
先获取锁,如果失败,则订阅该锁的释放消息,一旦释放则收到通知立即获取,前提是在超时时间内收到通知,到了超时时间则会取消订阅,直接失败