1、除了Redis,你还知道其他分布式缓存方案吗?
redis痛点问题:内存占用高,数据可靠性差,业务维护缓存和存储一致性繁琐。
腾讯开源的Tendis也是分布式高性能KV存储数据库。
Tendis特征:
- 完全兼容Redis协议,支持绝大多数redis的指令
- 持久化存储:使用rocksdb作为存储引擎
- 去中心化架构:类似于redis cluster分布式实现,所有节点通过gossip协议通讯,可指定hashtag来控制数据分布和访问,使用和运维成本极低。
- 水平拓展:集群支持增删节点,并且数据可以按照slot在任意两节点之间歉意,扩容和缩放过程中对应用韵味人员透明,支持拓展至1000个节点。
- 故障高可用:自动检测故障节点,当故障发生,slave会提升为master继续对外服务。
- redis冷热混合存储关键组件:得益于
Tendis存储版
的设计和内部优化,Redis
和Tendis存储版
可以一起工作成为Tendis冷热混合存储
。混合存储区非常适用于KV存储场景,并平衡了性能和成本。对于redis,占用大量存储空间的冷数据降冷后可以最多减少80%的成本,同时保证热数据在redis的访问性能。
选择redis原因:经历过多年不断考验、生态优秀、资料全面
为什么要用redis?
- 访问速度快
redis基于内存,内存的访问速度比磁盘快很多,引入Redis后,可以把高频访问的数据放到Redis中,下次直接从内存中读取,速度提升几十倍甚至上百倍。
- 高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数。
可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
- 功能全面
redis除了缓存之外,还可用于分布式锁、限流、消息队列、延时队列等场景。
2、常见的缓存读写策略有哪些?
包括:旁路缓存、读写穿透、异步缓存写入
1. Cashe Aside Pattern (旁路缓存模式)
缓存读写模式,比较适合读请求比较多的场景
其中服务端需要同时维系 db 和 cache,并且是以db的结果为准。
写:
- 先更新db
- 然后直接删除cache。
读:
- 从cache中读取数据,读取到就直接返回
- cache中读取不到的话,就从db中读取数据返回
- 再把数据放到cache中
问题:
在写数据的过程中,可以先删除cache,后更新db吗?
答案:不行,这样会造成db和缓存不一致问题,比如,请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新
在写数据的过程中,先更新db,后删除cache就没有问题了吗?
答案:理论上来说还是有可能出现数据不一致问题,不过概率非常小,因为缓存写入的速度比数据库的写入速度快很多。简单理解为,请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache
Cashe Aside Pattern 存在缺陷:
- 首次请求数据一定不在cache问题
解决办法:将热点数据提前放到cache中
- 写操作比较繁琐的话,导致cache中的数据会被频繁删除,这样会影响缓存命中率。
解决办法:
数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
2. Read/Write Through Pattern (读写穿透)
服务端把cache视为主要数据存储,从中读取数据并将数据写入其中。cache服务负责将此数据读取和写入db,从而减轻了应用程序的职责。
写:
- 先查 cache,cache 中不存在,直接更新 db。
- cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
读:
- 从 cache 中读取数据,读取到就直接返回 。
- 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
3. Write Behind Pattern (异步缓存写入)
Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
面临问题:比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就挂掉了。
应用:消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
3、Redis除了做缓存,还能做什么?
- 分布式锁
- 限流:通过Redis + Lua 脚本的方式实现限流
- 消息队列:支持消息持久化、ACK机制
- 延时队列:Redisson内置了延时队列
- 分布式Session:利用String 或 Hash数据类型
- 复杂业务业务场景:通过Redis 以及 Redis拓展(Redisson)提供的数据结构。业务:通过Bitmap统计活跃用户、通过Sorted Set 维护排行榜
4、如何基于Redis实现分布式锁?
SETNX
如果key不存在,设置key的值,如果key已存在,SETNX
啥也不做。
SETNX
命令加锁:如果对应的key不存在则加锁,如果存在那么获取锁失败
基于Lua脚本释放锁:判断key(锁)对应的value是否相等 --> 执行DEL
命令释放锁,如果不相等,那么释放锁失败。
存在问题:释放锁逻辑的程序突然挂掉,可能会导致锁无法被释放,造成共享资源无法再被其他线程/进程访问。
5、为什么要给锁设置一个过期时间?
解决问题:避免锁无法被释放 --> 给这个key(锁)设置一个过期时间。
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
lockKey:加锁的锁名;
uniqueValue:能够唯一标识锁的随机字符串;
NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
存在:共享资源操作时间大于过期时间,就会出现锁提前过期的情况,如果锁的时间设置过长,会影响程序性能。
6、如何合理的设置锁的过期时间?
Redisson(Java语言Redis客户端) 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
this.lockWatchdogTimeout = lockWatchdogTimeout;
return this;
}
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
主要逻辑
private void renewExpiration() {
//......
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//......
// 异步续期,基于 Lua 脚本
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 无法续期
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 递归调用实现续期
renewExpiration();
} else {
// 取消续期
cancelExpirationRenewal(null);
}
});
}
// 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
默认情况下,没过10s看门狗就会执行续期操作,将锁的超时时间设置为30s。
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
调用renewExpirationAsync()
方法实现锁的异步续期
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
使用Redisson分布式可重入锁RLock
为例来说明如何使用Redisson实现分布式锁:
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);
7、如何基于Redis实现延时任务?
场景:订单在 10 分钟后未支付就失效、红包 24 小时未被查收自动退还
两种方案:1. Redis过期时间监听;2. Redisson内置的延时队列
Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
Redisson 内置的延时队列具备下面这些优势:
- 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。也可以使用扫描数据库的方法作为补偿机制。
- 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
8、Redisson延迟队列原理是什么?有什么优势?
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。
借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。
Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。
Redisson 使用 zrangebyscore
命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。
跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。
标签:总结,缓存,过期,Redis,cache,db,面试,Redisson From: https://blog.csdn.net/m0_74119287/article/details/143374733