zoukankan      html  css  js  c++  java
  • redis解決高并发分布式锁相关学习

    引出问题

    高并发环境下还有如下三个问题

    1. 如果redis宕机了,或者链接不上,怎么办?
    2. 如果redis缓存在高峰期到期失效,在这个时刻请求会向雪崩一样,直接访问数据库如何处理?
    3. 如果用户不停地查询一条不存在的数据,缓存没有,数据库也没有,那么会出现什么情况,如何处理?
    这里可以引出缓存的几个术语

    缓存问题术语

    1.缓存穿透

    缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,并且处于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

    解决

    空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

    2.缓存雪崩

    缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

    解决:

    原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

    3.缓存击穿

    对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

    和缓存雪崩的区别:

    击穿是一个热点key失效

    雪崩是很多key集体失效

    解决:

    分布式锁

    那么问题来了,什么是分布式锁?如何实现分布式锁?

    分布式锁

    是什么?

    为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁

    分布式锁应该具备哪些条件

    • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
    • 高可用的获取锁与释放锁
    • 高性能的获取锁与释放锁
    • 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
    • 具备锁失效机制,防止死锁
    • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

    分布式锁的实现有哪些

    • Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。
    • Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。
    • Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
    • Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

    通过 Redis 分布式锁的实现理解基本概念

    加锁

    最简单的方法是使用 setnx 命令。key 是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给 key 命名为 “lock_sale_商品ID” 。而 value 设置成什么呢?我们可以姑且设置成 1。加锁的伪代码如下:

    setnx(lock_sale_商品ID,1)
    

    当一个线程执行 setnx 返回 1,说明 key 原本不存在,该线程成功得到了锁;当一个线程执行 setnx 返回 0,说明 key 已经存在,该线程抢锁失败。

    解锁

    有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del 指令,伪代码如下:

    del(lock_sale_商品ID)
    

    释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。

    锁超时

    锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住(死锁),别的线程再也别想进来。所以,setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx 不支持超时参数,所以需要额外的指令,伪代码如下:

    expire(lock_sale_商品ID, 30)
    

    综合伪代码如下:

    if(setnx(lock_sale_商品ID,1) == 1){
        expire(lock_sale_商品ID,30)
        try {
            do something ......
        } finally {
            del(lock_sale_商品ID)
        }
    }
    

    存在问题

    1.setnx 和 expire 的非原子性
    设想一个极端场景,当某线程执行 setnx,成功得到了锁
    在这里插入图片描述
    setnx 刚执行成功,还未来得及执行 expire 指令,节点 1 挂掉了
    在这里插入图片描述
    这样一来,这把锁就没有设置过期时间,变成死锁,别的线程再也无法获得锁了。

    怎么解决呢?setnx 指令本身是不支持传入超时时间的,set 指令增加了可选参数,伪代码如下:

    set(lock_sale_商品ID,1,30,NX)
    

    这样就可以取代 setnx 指令。
    2.del 导致误删
    又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是 30 秒。
    在这里插入图片描述
    如果某些原因导致线程 A 执行的很慢很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
    在这里插入图片描述
    随后,线程 A 执行完了任务,线程 A 接着执行 del 指令来释放锁。但这时候线程 B 还没执行完,线程A实际上 删除的是线程 B 加的锁。
    在这里插入图片描述
    怎么避免这种情况呢?可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。至于具体的实现,可以在加锁的时候把当前的线程 ID 当做 value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。

    加锁:

    String threadId = Thread.currentThread().getId()
    set(key,threadId ,30,NX)
    

    解锁:

    if(threadId .equals(redisClient.get(key))){
        del(key)
    }
    

    但是,这样做又隐含了一个新的问题,判断和释放锁是两个独立操作,不是原子性。

    3.出现并发的可能性
    还是刚才第二点所描述的场景,虽然我们避免了线程 A 误删掉 key 的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
    在这里插入图片描述

    当过去了 29 秒,线程 A 还没执行完,这时候守护线程会执行 expire 指令,为这把锁“续命”20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。
    在这里插入图片描述
    当线程 A 执行完任务,会显式关掉守护线程。
    在这里插入图片描述
    另一种情况,如果节点 1 忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
    在这里插入图片描述

  • 相关阅读:
    创建类以及引用一个类
    修改hosts文件
    微信第三方登录接口开发
    Android定位
    Leetcode 102. Binary Tree Level Order Traversal
    Leetcode 725. Split Linked List in Parts
    Leetcode 445. Add Two Numbers II
    Leetcode 328. Odd Even Linked List
    Leetcode 237. Delete Node in a Linked List
    Leetcode 234. Palindrome Linked List
  • 原文地址:https://www.cnblogs.com/faramita/p/11974808.html
Copyright © 2011-2022 走看看