[转][摘]
分布式之redis复习精讲
分布式之数据库和缓存双写一致性方案解析
分布式之缓存击穿
1.使用redis有什么缺点:
(一)缓存和数据库双写一致性问题
(二)缓存雪崩问题
(三)缓存击穿问题
(四)缓存的并发竞争问题
2.单线程的redis为什么这么快:
(一)纯内存操作
(二)单线程操作,避免了频繁的上下文切换
(三)采用了非阻塞I/O多路复用机制
3.redis的数据类型,以及每种数据类型的使用场景:
String、Hash、List、Set、SortedSet
4.redis的过期策略以及内存淘汰机制:
redis采用的是定期删除+惰性删除策略
5.redis和数据库双写一致性问题:
- 第一种方案:采用延时双删策略
- 第二种方案:异步更新缓存(基于订阅binlog的同步机制)
6.如何应对缓存穿透问题:
缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
【解决方案】:
- (一) 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。 比如:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
- (二) "永远"不过期:采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
- (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
- (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}
-
(三) 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
-
(四) 资源保护:采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。优点:hystrix技术成熟,有效保证后端;监控强大,缺点:部分访问存在降级策略。
解决方案没有最佳,只有最合适!
7.如何应对缓存雪崩问题:
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决方案:
(一)给缓存的失效时间,加上一个随机值,避免集体失效。
(二)使用互斥锁,但是该方案吞吐量明显下降了。
(三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
I 从缓存A读数据库,有则直接返回
II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
III 更新线程同时更新缓存A和缓存B。
8.如何解决redis的并发竞争key问题:
(不推荐使用redis的事务机制,redis集群环境会做数据分片,多个key不一定都存储在同一个redis-server上,很鸡肋)
(1) 如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2) 如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
其他方法,比如利用队列,将set方法变成串行访问也可以。总之,灵活变通。
9.Redis 添加分布式锁
incr、incrBy、setnx 的加锁方式都是有缺陷的,还可以使用 set 方法加锁:
// "NX" 表示如果Redis中不存在key时,就设置key,"EX" 表示设置超时,expireSeconds 表示超时的值
jedisCluster.set(key, value, "NX", "EX", expireSeconds); // SET IF NOT EXIST
// jedis 源代码
@Override
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
return new JedisClusterCommand<String>(connectionHandler, maxRedirections) {
@Override
public String execute(Jedis connection) {
return connection.set(key, value, nxxx, expx, time);
}
}.run(key);
}
此种方式添加锁后的解锁也有特殊地方,需要比较 请求解锁线程 是否是 当时加锁 的线程。可参见:Redission 实现 Redis Redlock 分布式锁#1. 前言 -- 普通分布式锁实现.
而且还是原子的。
可参考https://blog.csdn.net/Dennis_ukagaka/article/details/78072274,其实jedis的每个可能会新增的操作都应该有这么一个与时间相关的原子性方法,不然还要我们自己写lua脚本。
另外有大神【芋道源码】文章:一文看透 Redis 分布式锁进化史(解读 + 缺陷分析)
10.Redis 踩坑
- AOF
Redis 的AOF机制有点类似于Mysql binlog,是Redis的提供的一种持久化方式(另一种是RDB),它会将所有的写命令按照一定频率(no, always, every seconds)写入到日志文件中,当Redis停机重启后恢复数据库。 - AOF重写
1.随着AOF文件越来越大,里面会有大部分是重复命令或者可以合并的命令(100次incr = set key 100)
2.重写的好处:减少AOF日志尺寸,减少内存占用,加快数据库恢复时间。 - 单机多实例可能存在
Swap
和OOM
的隐患
由于Redis的单线程模型,理论上每个redis实例只会用到一个CPU, 也就是说可以在一台多核的服务器上部署多个实例(实际就是这么做的)。但是Redis的AOF重写是通过fork出一个Redis进程来实现的,所以有经验的Redis开发和运维人员会告诉你,在==一台服务器上要预留一半的内存==(防止出现AOF重写集中发生,出现swap和OOM)。 - meta信息
作为一个redis云系统,需要记录各个维度的数据,比如:业务组、机器、实例、应用、负责人多个维度的数据,相信每个Redis的运维人员都应该有这样的持久化数据(例如Mysql),一般来说还有一些运维界面,为自动化和运维提供依据
评论区