1. Redis分布式锁
1.1 Redis实现分布式锁
Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争关系 Redis 中可以使用 SETNX 命令实现分布式锁。
- 实现:
- 但是该分布式锁会产生一定的问题:
- 死锁问题: 异常没释放锁
- 误删锁:线程一 误删了线程二的锁
- 解决方案:
- Redisson
Redisson 是一个高级的分布式协调 Redis 客服端,能帮助用户在分布式环境中轻松实现一些 Java 的对象
- 执行流程:
1.2 RedLock(红锁)
- Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫Redlock
- 此种方式比原先的单节点的方法更安全。它可以保证以下特性:
- 安全特性:互斥访问,即永远只有一个 client 能拿到锁
- 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
- 容错性:只要大部分 Redis 节点存活就可以正常提供服务
2. Redis使用问题
2.1 缓存穿透
- 缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
- 导致缓存穿透的原因:
缓存穿透可能有两种原因:
- 自身业务代码问题
- 恶意攻击,爬虫造成空命中
- 解决方法:
- 接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将
空结果(null)进行缓存
,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
- 布隆过滤器:
布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。
存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。
- 布隆过滤器我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是1:
- 如果全不是1,那么key不存在;
- 如果都是1,也只是表示key可能存在。
- 布隆过滤器也有一些缺点:
它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。
不支持删除元素。
2.2 缓存击穿
- 一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。
1. 预先设置热门数据
:在redis高峰访问前,把一些热门数据提前存入redis中,加大这些热门数据key的时长,实时调整现场监控哪些数据是热门数据,实时调整key的过期时长或者不设置过期时间
2.使用分布式锁
: 就是在缓存失效的时候(判断拿出来的值为空),不是立即去查数据库。比如redis的setnx去set一个mutex key,当操作返回成功时(分布式锁),在查数据库,并回设缓存,最后删除mutex key;当操作返回失败,证明有线程在查询数据库,当前线程睡眠一段时间在重试整个get缓存的方法。
2.3 缓存雪崩
- 某⼀时刻发⽣⼤规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是⼤量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。
A.)构建多级缓存架构
:nginx缓存+redis缓存+其他缓存(ehcache等)
使用锁或队列:使用锁或在队列的方式来保证不会有大量的线程对数据库进行读写,从而避免失效时大量的并发请求到底层存储系统上,不适用高并发情况。
B.)将缓存失效时间分散开
:设置缓存过期时间时加上一个随机值,避免缓存在同一时间过期
- 缓存雪崩是三大缓存问题里最严重的一种,我们来看看怎么预防和处理。
- 提高缓存可用性
- 集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
- 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
- 过期时间
- 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
- 热点数据永不过期。
- 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
- 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存
- 熔断降级
- 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
- 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。
2.4 缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
- 所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:
1、直接写个缓存刷新页面或者接口,上线时手动操作
2、数据量不大,可以在项目启动的时候自动进行加载
3、定时任务刷新缓存.
2.5 多个命令如何保证原子性
-
- 使用Lua脚本
- Redis的事务功能比较简单,平时的开发中,可以利用Lua脚本来增强Redis的命令。
Lua脚本能给开发人员带来这些好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
- 比如这一段很经典的秒杀系统利用lua扣减Redis库存的脚本:
-- 库存未预热
if (redis.call('exists', KEYS[2]) == 1) then
return -9;
end;
-- 秒杀商品库存存在
if (redis.call('exists', KEYS[1]) == 1) then
local stock = tonumber(redis.call('get', KEYS[1]));
local num = tonumber(ARGV[1]);
-- 剩余库存少于请求数量
if (stock < num) then
return -3
end;
-- 扣减库存
if (stock >= num) then
redis.call('incrby', KEYS[1], 0 - num);
-- 扣减成功
return 1
end;
return -2;
end;
-- 秒杀商品库存不存在
return -1;
-
- Redis的事务
- Redis提供了简单的事务,但它对事务ACID的支持并不完备。
- multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的:
- Redis事务的注意点有哪些?
需要注意的点有:
- Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
- Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
- Redis 事务为什么不支持回滚?
- Redis 的事务不支持回滚。
- 如果执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。
- 这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
Redis 单线程?多线程?
- Redis的速度⾮常的快,单机的Redis就可以⽀撑每秒十几万的并发,相对于MySQL来说,性能是MySQL的⼏⼗倍。速度快的原因主要有⼏点:
- 完全基于内存操作
- 使⽤单线程,避免了线程切换和竞态产生的消耗
- 基于⾮阻塞的IO多路复⽤机制
- C语⾔实现,优化过的数据结构,基于⼏种基础的数据结构,redis做了⼤量的优化,性能极⾼
4. 缓存与数据库双写时的数据一致性
- 根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。
- 缓存不一致处理
- 如果不是并发特别高,对缓存依赖性很强,其实一定程序的不一致是可以接受的。
- 但是如果对一致性要求比较高,那就得想办法保证缓存和数据库中数据一致。
- 缓存和数据库数据不一致常见的两种原因:
- 缓存key删除失败
- 并发导致写入了脏数据
- 弱一致:
延时双删
机制:增删改时删除缓存中的数据,再过2-3S后再删一次,延时的目的是为了构建缓存的完成,把缓存构建完成后再进行删除。 缺点:会造成服务器阻塞,不适合高并发环境。- 使用
canal监听数据库
的变化
因为redis执行顺序和mysql的执行顺序不一致导致的,使用alibaba的服务器canal数据库监听服务,日志的顺序是固定的,监控mysql的Bin-log日志,解析binlog得到操作前后的操作数据,该日志是记录mysql每次commit后记录,此时mysql有行锁,其他的线程无法操作只能等待,每次操作时,把自己伪装成mysql的从机,主机会主动把操作的任务执行发给从机,使用canal的客户端来操作redis;- 延迟双删中第二次删除为什么要延迟一会,为什么不直接删除?
在并发环境下,确保能够删除其他请求在缓存中存入的旧值,保证我们构建缓存的完整性。
- 删除缓存失败怎么办?
- 强一致:
5. Redis应用:
5.1 使用Redis 实现异步队列
- 使用 list 类型保存数据信息,lpush 生产消息,rpop 消费消息,当 rpop 没有消息时,可以 sleep 一段时间,然后再检查有没有信息,但是这样又会有消息的延迟问题。
- 如果不想 sleep 的话,可以使用 blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。这种方式只能实现一对一的消息队列。
- redis 可以通过 pub/sub 主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。所以,一般的异步队列的实现还是交给专业的消息队列。
5.2 使用Redis 实现延时队列
- 使用zset,利用排序实现
- 使用 sortedset,使用时间戳做 score, 消息内容作为 key,调用 zadd 来生产消息,消费者使用 zrangbyscore 获取 n 秒之前的数据做轮询处理。
- 可以使用 zset这个结构,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 …命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。