1、Redis 事务
👨💻 :说一下你对 redis 事务的了解呗。
Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH
等命令来实现事务功能。
- 事务流程
- 开始事务(MULTI)。
- 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行)。
- 执行事务(EXEC)。
你也可以通过 DISCARD
命令取消一个事务,它会清空事务队列中保存的所有命令。
WATCH
命令用于监听指定的键,当调用 EXEC
命令执行事务时,如果一个被 WATCH
命令监视的键被修改的话,整个事务都不会执行,直接返回失败。
Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。
2、如何实现分布式锁?
👨💻 :高并发下分布式系统中的数据线程安全怎么保障?
① 加锁
/**
* 加锁
* @param key
* @param value
* @return
*/
public boolean lock(String key,String value){
if(redisTemplate.opsForValue().setIfAbsent(key,value)){
// 加锁成功
return true;
}
// 如果锁过期
String currentValue = redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if(!StringUtils.isEmpty(oldValue) &&
oldValue.equals(oldValue)){
return true;
}
}
// 锁已经被其它线程获取
return false;
}
- setIfAbsent:如果对应的
key
不存在的话,那么就进行相关设置,也就是加锁成功。同时我们设置进行的value
是对应的key
的过期时间。 - 过期时间的判断:如果对应的锁被相关的其它进程获取,我们还要判断对应的锁是否过期。如果过期的话,那么其它的线程就可以进行争抢。
- getAndSet:获取旧值设置新值,该操作是原子性的。
- oldValue.equals(oldValue):该判断尤其重要,因为我们此时是高并发状态下,那么就有可能出现对应的线程争抢情况,但是我们只能允许一个线程获取锁,那么我们就要对其进行
oldValue
的判断,如果是相同的话,那么就获取锁成功,否则就是被其它线程争抢过去了。
② 解锁
- 查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,然后被其他线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁”
- 如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value){
try{
String currentValue = redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) &&
currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis 分布式锁】 解锁异常,{}",e);
}
}
3、缓存穿透
👨💻 :简要说一下缓存穿透现象和怎样应对 ?
- 描述:访问一个缓存和数据库都不存在的 key,此时请求会直接打到数据库上,并且数据库查不到数据,也没办法写入缓存,所以下一次请求同样会打到数据库上。
解决方案
- 接口校验:后端接口增加数据合理性校验,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤。
- 缓存空值:当访问缓存和DB都没有查询到值时,可以将空值写进缓存,为其设置短点的过期时间,防止同一个 key 被一直攻击。
- 布隆过滤器:使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。
4、缓存击穿
👨💻 :简要说一下缓存击穿现象和怎样应对 ?
- 描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
解决方案
- 设置热点数据不过期,或者定时任务定时更新缓存。
- 设置互斥锁,在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。(可以使用
Redis 分布式锁
)
5、缓存雪崩
👨💻 :简要说一下缓存雪崩现象和怎样应对 ?
- 描述:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。
解决方案
- 加互斥锁: 该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
- 热点数据不过期:该方式和缓存击穿一样。
- 过期时间打散:既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
6、Redis 常见性能问题和解决方案
👨💻 :你了解redis 常见性能问题和解决方案吗?
- Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度rdbSave 函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务
- 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <-Slave1<- Slave2 <- Slave3…这样的结构方便解决单点故障问题,实现 Slave对 Master 的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。
7、Redis 如何做内存优化?
👨💻 :redis 如何做内存优化?
尽可能使用散列表,散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面
。
8、如何保证 redis 中的数据都是热点数据?
👨💻 :MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据?
- Redis 提供 6 种数据淘汰策略:
volatile-lru
:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰volatile-ttl
:从已设置过期时间的数据集中挑选将要过期的数据淘汰volatile-random
:从已设置过期时间的数据集中任意选择数据淘汰allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)allkeys-random
:从数据集中任意选择数据淘汰no-eviction
:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
- 4.0 版本后增加以下两种:
volatile-lfu
:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰allkeys-lfu
:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
9、Redis 中如何将指定模式的 key 查找出来?
👨💻 :假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
- 使用 keys 指令可以扫出指定模式的 key 列表。
👨💻 :如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?
- redis 的单线程的。keys 指令会导致线程阻塞一段时间,**线上服务会停顿,直到指令执行完毕,服务才能恢
复**。
- 这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。