【Redis分布式锁+Redis使用问题+Redis单线程?多线程?+缓存与数据库双写时的数据一致性】

1. Redis分布式锁

1.1 Redis实现分布式锁

Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争关系 Redis 中可以使用 SETNX 命令实现分布式锁。

  • 实现:

在这里插入图片描述

  • 但是该分布式锁会产生一定的问题:
  1. 死锁问题: 异常没释放锁
  2. 误删锁:线程一 误删了线程二的锁
  3. 解决方案:
    在这里插入图片描述
  • Redisson

Redisson 是一个高级的分布式协调 Redis 客服端,能帮助用户在分布式环境中轻松实现一些 Java 的对象

在这里插入图片描述

  • 执行流程:

在这里插入图片描述
在这里插入图片描述

1.2 RedLock(红锁)

  • Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫Redlock
  • 此种方式比原先的单节点的方法更安全。它可以保证以下特性:
  1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务

2. Redis使用问题

2.1 缓存穿透

  • 缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

在这里插入图片描述

  • 导致缓存穿透的原因:

缓存穿透可能有两种原因:

  1. 自身业务代码问题
  2. 恶意攻击,爬虫造成空命中
  • 解决方法:
  1. 接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 空结果(null)进行缓存,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
  • 布隆过滤器:

布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。
存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。

在这里插入图片描述

  • 布隆过滤器我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是1:
  1. 如果全不是1,那么key不存在;
  2. 如果都是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.)将缓存失效时间分散开:设置缓存过期时间时加上一个随机值,避免缓存在同一时间过期

  • 缓存雪崩是三大缓存问题里最严重的一种,我们来看看怎么预防和处理。
  • 提高缓存可用性
  1. 集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
  2. 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
  • 过期时间
  1. 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
  2. 热点数据永不过期。
  3. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  4. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存
  • 熔断降级
  1. 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
  2. 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。

2.4 缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

  • 所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:

1、直接写个缓存刷新页面或者接口,上线时手动操作
2、数据量不大,可以在项目启动的时候自动进行加载
3、定时任务刷新缓存.

2.5 多个命令如何保证原子性

在这里插入图片描述

    1. 使用Lua脚本
  • Redis的事务功能比较简单,平时的开发中,可以利用Lua脚本来增强Redis的命令。

Lua脚本能给开发人员带来这些好处:

  1. Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
  2. Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
  3. 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;
    1. Redis的事务
  • Redis提供了简单的事务,但它对事务ACID的支持并不完备。
  • multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的:
  • Redis事务的注意点有哪些?

需要注意的点有:

  1. Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
  2. Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
  • Redis 事务为什么不支持回滚?
  1. Redis 的事务不支持回滚。
  2. 如果执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。
  3. 这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。

Redis 单线程?多线程?

  • Redis的速度⾮常的快,单机的Redis就可以⽀撑每秒十几万的并发,相对于MySQL来说,性能是MySQL的⼏⼗倍。速度快的原因主要有⼏点:
  1. 完全基于内存操作
  2. 使⽤单线程,避免了线程切换和竞态产生的消耗
  3. 基于⾮阻塞的IO多路复⽤机制
  4. C语⾔实现,优化过的数据结构,基于⼏种基础的数据结构,redis做了⼤量的优化,性能极⾼
    在这里插入图片描述
    在这里插入图片描述

4. 缓存与数据库双写时的数据一致性

  • 根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。
    在这里插入图片描述
  • 缓存不一致处理
  1. 如果不是并发特别高,对缓存依赖性很强,其实一定程序的不一致是可以接受的。
  2. 但是如果对一致性要求比较高,那就得想办法保证缓存和数据库中数据一致。
  • 缓存和数据库数据不一致常见的两种原因:
  1. 缓存key删除失败
  2. 并发导致写入了脏数据
  • 弱一致:

在这里插入图片描述

  1. 延时双删机制:增删改时删除缓存中的数据,再过2-3S后再删一次,延时的目的是为了构建缓存的完成,把缓存构建完成后再进行删除。 缺点:会造成服务器阻塞,不适合高并发环境。
  2. 使用canal监听数据库的变化
    因为redis执行顺序和mysql的执行顺序不一致导致的,使用alibaba的服务器canal数据库监听服务,日志的顺序是固定的,监控mysql的Bin-log日志,解析binlog得到操作前后的操作数据,该日志是记录mysql每次commit后记录,此时mysql有行锁,其他的线程无法操作只能等待,每次操作时,把自己伪装成mysql的从机,主机会主动把操作的任务执行发给从机,使用canal的客户端来操作redis;
  3. 延迟双删中第二次删除为什么要延迟一会,为什么不直接删除?
    在并发环境下,确保能够删除其他请求在缓存中存入的旧值,保证我们构建缓存的完整性。
  4. 删除缓存失败怎么办?
    在这里插入图片描述
  • 强一致:

在这里插入图片描述

5. Redis应用:

5.1 使用Redis 实现异步队列

  1. 使用 list 类型保存数据信息,lpush 生产消息,rpop 消费消息,当 rpop 没有消息时,可以 sleep 一段时间,然后再检查有没有信息,但是这样又会有消息的延迟问题。
    在这里插入图片描述
  2. 如果不想 sleep 的话,可以使用 blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。这种方式只能实现一对一的消息队列。
    在这里插入图片描述
  3. redis 可以通过 pub/sub 主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。所以,一般的异步队列的实现还是交给专业的消息队列。
    在这里插入图片描述

5.2 使用Redis 实现延时队列

  • 使用zset,利用排序实现
  1. 使用 sortedset,使用时间戳做 score, 消息内容作为 key,调用 zadd 来生产消息,消费者使用 zrangbyscore 获取 n 秒之前的数据做轮询处理。
  2. 可以使用 zset这个结构,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 …命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。
    在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值