redis常见问题及处理机制
Redis
简介
数据库 redis数据在内存中,读写速度快,广泛用于缓存方向
- 为什么要使用缓存(redis) ->
高性能: (传统数据库 - 磁盘读取内存页交换等等...)
高并发: 能够承受的请求远远大于直接访问数据库的- 缓存分为本地缓存和分布式缓存
分布式缓存:各实例共用一分缓存数据,缓存具有一致性
本地缓存:自带map或者guava,轻量快速,多实例情况下,各自保存一份缓存,缓存不具有一致性- redis工作模型
单线程工作模型,串行
redis内部使用文件事件处理器,I/O多路复用机制
避免上下文多切换
redis内存模型:
https://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/81140409
一、Redis缓存失效机制
关键字 Expire
目的: 减轻后端数据库的压力,借助Redis服务把变化频率不高的数据从DB load出来放入缓存,但是一段时间后再重新从DB load出当前的数据
指令: EXPIRE key 30
延迟实效机制(惰性删除)
Redis会对客户端请求操作的key进行有效期校验,如果key过期才进行相应处理
主动失效机制(定期删除)
服务端定时的去检查失效的缓存(不是检查所有的key,而是随机抽取进行检查),如果失效则进行相应的操作
Redis是单线程,基于事件驱动;
redis在启动的时候,会注册两种事件,时间事件+文件事件(处理请求)
Redis中有个EventLoop负责对两类事件进行处理:
a. IO事件
b. 定时任务
线程模型:https://segmentfault.com/a/1190000019910205?utm_source=tag-newest
设置了过期时间,但是时间到了,内存占用还是挺高的
- 内存淘汰机制
如果大量过期的key堆积在内存中,导致redis内存耗尽- Mysql里有2000w条数据,redis中只存20W数据,如何保证redis中的数据都是热点数据?
redis中提供6种数据淘汰策略
但是如果正好淘汰之后,又大并发的请求了某一个非热点数据
typedef struct redisDb{
dict *dict;
dict *expires;
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
long long avg_ttl
} redisDb
为什么要使用缓存
所有请求直接访问数据库,数据库会出现连接异常(如果真的全部直接访问即所有的缓存都失效的话怎么办??)
中间加一个缓存,大家先访问到缓存
mybatis一级缓存-sqlsession 二级缓存-mapper
单线程Redis为什么这么快
a 纯内存操作
b 单线程操作,避免上下文切换
c 采用了非阻塞I/O多路复用机制
文件事件分派器
事件处理器
传统的并发模型:
a 每个I/O流都有一个新的线程管理
b I/O多路复用,只有单个线程,通过跟踪每个I/O流的状态,来管理多个I/O流
缓存和数据库双写一致性问题
强一致性 和 最终一致性
如果对数据有强一致性要求,不能放缓存
一、先更新数据库,再删除缓存
异常情况:A和B同时请求
1、A更新了数据库
2、B更新了数据库
3、B更新了缓存
4、A更新了缓存
二、先删缓存、再更新数据库
异常情况:A进行更新操作,B进行查询操作
1、 请求A进行写操作,删除缓存
2、 B查询发现缓存不存在
3、 B去数据库查询得到旧值
4、 B将旧值写入缓存
5、 请求A将新值写入缓存
三、先更新数据库,再删除缓存-推荐
异常情况:A进行查询操作,B进行更新操作
1、缓存刚好失效
2、请求A查询数据库,得一个旧值
3、请求B将新值写入数据库
4、请求B删除缓存
5、请求A将查到的旧值写入缓存
延时双删策略:
更新数据库 -> 删除缓存 -> 需要删除的key -> 进入消息队列 -> 需要删除的key -> 重试删除操作
缓存雪崩问题
大面积失效
解决方案:
- 给缓存的失效时间,加一个随机值,避免集体失效
- 利用互斥锁,但该方案的吞吐量下降
- 双缓存。我们有两个缓存; 缓存A和缓存B。缓存A的失效时间为20min,缓存B不设失效时间,自己做缓存预热操作
缓存击穿问题
请求缓存中不存在的数据,导致所有的请求打到数据库上,从而导致数据库异常(连接数,锁,资源抢占)
解决方案:
- 利用互斥锁, 先去获得锁,得到锁再去请求数据库,没得锁休眠再重试
- 采用异步更新策略,无论key是否取到值,都直接返回。Value值中维护一个缓存失效时间,缓存如果过去,异步起一个线程去读数据库,更新缓存,需要做缓存预热
- 提供一个能迅速判断请求是否有效的拦截机制,比如说维护一个有效的key list
- 布隆过滤器:
4.1 布隆过滤器数据结构和原理
a、一种概率型数据结构,特点是高效插入和查询,更高效占用空间更少
b、返回结果是概率性的,而不是确切的
String get(String key){
String value = redis.get(Key);
if (Value == null){
if(!bloomfilter.mightCotain(key)){
return null;
}else{
value = db.get(key);
redis.set(key,value)
}
}
return value;
}
热点key问题,bigKey问题
某些节点的连接数、内存,cpu明显高于其它节点->导致物理上线,直接怼到数据库上
热key监控统计、多级缓存、本地缓存、散列这个热点key(多个redis上都存一份)
优化、限制、隔离
https://www.jianshu.com/p/58615a1e2cac
https://blog.csdn.net/u010522235/article/details/89241765
缓存的并发竞争问题
如果key操作,不要求顺序
给一个分布式锁
如果对key操作,要求顺序
写要求时 {数据:timestamp}
key是顺序变化的,写入数据库时,需要保存一个时间戳
redis适合场景
- 会话缓存(Session Cache)
Redis提供持久化,如购物车信息
- 全页缓存(FPC)
如果基于服务端内容进行渲染,可以使用redis把常被请求的内容缓存起来,能够大大降低页面请求的延迟
set key <html>...</html> EX 60
- 队列 list 、 set
- 排行榜/计数器
Redis在内存中对数字进行递增或递减操作实现非常友好,集合(Set)和有序集合(Sorted Set)
zadd sortedSet 1 "one"
ZRNGE sortedSet 0 -1
ZRANGE sortedSet 0 -1 withscores
- 发布/订阅
聊天系统 社交网络的通知触发器
- Session存储
- 秒杀
原子性 ,分布式,消峰,分布式锁,分布式数据
- 分布式锁
与分布式相对应的是线程锁,进程锁
分布式锁:多个进程不在同一个系统中
线程锁:同一JVM,比如Synchronized(管锁)、Lock
进程锁: 为了控制同一操作系统中多个进程访问某个资源,各进程无法访问其他进程资源,所以不能用Synchronized加锁
接口的幂等性:简单来说,就是对接口多次调用所产生的结果和调用一次是一致的?
弱网络下的重复请求,如何保证幂等性
在分布式环境下,网络环境更加复杂:
因前端操作抖动,网络故障,消息重复,响应速度等原因,对接口的重复调用概率会比集中式环境下更大,尤其是重复消息在分布式环境中很难避免
分布式锁实现策略:
a、数据修改->数据库-> 乐观锁-cAS
b、 redis防重策略
Set key value NX EX max-lock-time
SETNX老黄历
2.6.12就可以使用set来获取锁,Lua脚本来释放锁
Redis分布式锁实现的开源框架:Redisson
RLock lock = redisson.getLock("myLock");
lock.lock();
lock.unlock();
1. 加锁机制,如果某个客户端要加锁,该客户端面对的是一个redis cluster集群,首先根据hash节点选择**一台机器**或者根据随机值到某一个分片prefix_key+shardNumber
2. 发送一段lua脚本到redis上
3. 后续判断有没有锁,锁过期时间...master slave主从复制
这个加锁方案,如果在异步复制锁信息的时候 redis master宕机,slave变成master,客户端来加锁会加锁成功,
这就导致了多个客户的对一个分布式锁完成了加锁
c、 zookeeper分布式锁实现
- 利用节点的唯一性原理
假设ABC三个客户端去访问资源,调用Zookeeper获得锁,客户端在三个在zookeeperde/lock节点下创建一个/lock节点,由于节点的唯一性分布式高并发情况下节点的创建一定能保证全局的唯一性,只有一个会创建成功,其他创建失败,然后其他监听这个节点,太多人监听就容易造成惊群事件,触发大量的watcher事件,此种方式不合理
- 第二种利用zookeeper的有序节点的特性()/
- 在获取分布式锁的时候,在locker节点下创建临时顺序节点,释放锁的时候删除该节点
- 客户端调用createNode方法在locks下创建临时节点,然后调用getChildren("locks)获取locks下的所有节点,主要此时不用设置任何Watcher
- 客户端获取所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁
- 如果发现自己创建的节点非locks的子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册监听事件
- 之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再判断是否是序号最小的,如果是则获取到锁,如果不是则重复以上步骤
https://www.jianshu.com/p/a974eec257e6
本地缓存和redis缓存的区别
参考链接:
https://zhuanlan.zhihu.com/p/21956773
https://zhuanlan.zhihu.com/p/58331707
Redis主从复制原理