hello,大家好,好久没有给大家分享过文章了,今天来给大家来点高并发中分布式锁的干货;好了,老规矩,废话少说,直接扔干货。
      分布式锁中准备给大家由浅入深讲解三种解决高并发中分布式锁的问题:

  • Redis实现分布式锁
  • Redission实现分布式锁

      下面我来依次给大家来进行分享:
      本文例子是围绕在高并发情况下,用户获取该商品详情,由于并发量过大,阻止请求全部打到数据库中,导致数据库宕机的情况发生。解决方法是引入缓存Redis,但是使用Redis之后一些列的问题发生,接下来我们就解决这一系列问题。

1、Redis实现分布式锁

      在高并发情况下直接访问数据库,肯定是不行的,会直接将数据库打宕机,那么这个时候就会直接导致系统瘫痪;可以引入redis缓存(这篇文章中不展开三级缓存讲解)来解决这个问题,引入方式代码如下(下面使用伪代码,只注重业务逻辑即可):

1.1、初步引入redis缓存

流程图如下所示:

分布式网站redis设计 redis做分布式_分布式锁

代码如下所示:

public String redisLock(Long id) {

    String key = "pro:detail:" + id;
    //从缓存中获取数据
    String resultStr = redisUtil.get(key, String.class);
    //缓存中没有拿到数据,去数据库中拿取数据
    if (StringUtils.isEmpty(resultStr)) {
        //进数据库获取数据
        System.out.println("从数据库中获取数据......");
        resultStr = "商品详情:查询数据库";
        //拿到数据后去将该数据放入缓存中
        redisUtil.set(key, resultStr + "存入缓存中!");
    } else {
        resultStr = "商品详情:查询缓存";
    }
    return resultStr;
}

      上述代码思路: 根据商品id查询时,先去缓存中拿取数据,如果缓存中获取到数据,直接将该数据返回;否则去数据库中获取数据,当从数据库获取到数据时,会将数据存放进缓存且将数据返回。
      上述思路看似没有问题,但是在高并发中存在如下问题:当大家同时从缓存中没有获取到时,那么大家都去数据库中获取数据,那么高并发情况下会将数据库直接打宕机
      解决上述问题方法: 可以引入分布式锁来解决大量请求访问数据库。
      思路: 只有当请求获取到可以访问数据库的锁时,才能访问数据库;也就是同一时段,请求同一条数据只有一个用户可以访问数据库,那么这个时候数据库就不会有那么大的压力。

1.2、引入分布式锁

      引入分布式锁逻辑流程图如下所示:

分布式网站redis设计 redis做分布式_分布式_02


代码如下所示:

String key = "pro:detail:" + id;
//从缓存中获取数据
String resultStr = redisUtil.get(key, String.class);
//缓存中没有拿到数据,去数据库中拿取数据
if (StringUtils.isEmpty(resultStr)) {

    /*存在问题:
        1、锁没有设置过期时间,那么当程序宕机时,将永远不会释放
        */

    //进数据库获取数据
    try {
        //尝试加锁
        boolean ifAbsent = redisUtil.setIfAbsent(key, null);
        if (ifAbsent) {
            System.out.println("从数据库中获取数据......");
            resultStr = "商品详情:查询数据库";
            //拿到数据后去将该数据放入缓存中
            redisUtil.set(key, resultStr + "存入缓存中!");
        } else {
            Thread.sleep(5000);
            //开启自旋
            this.redisLock(id);
        }
    } finally {
        //释放锁
        redisUtil.del(key);
    }
} else {
    resultStr = "商品详情:查询缓存";
}
return resultStr;

      上述代码思路: 当从缓存中无法获取数据时,按照上一版的代码是直接去查数据库,在这一版中做了更改,更改为不是随随便便就可以去查数据库的,只有拿到了分布式锁的请求才可以去访问数据库,最后在finally中释放锁
      存在问题: 当请求获取到锁,正在执行业务逻辑,当前还没有释放锁,但是这个时候程序挂掉了,那么该请求将永远拿到该锁不会再去释放,别的请求将无法访问数据库。
解决方法:在获取锁的时候添加锁的过期时间。

0.0.2版本

      直接添加过期时间,流程图我不再画了(因为和0.0.1b版本一样,只是多了过期时间),代码中也就只改动了一行,在set时增加了过期时间。

分布式网站redis设计 redis做分布式_java_03

代码逻辑: 整体逻辑和0.0.1版本逻辑一样,只是增加了过期时间,这样就能很好的解决刚刚0.0.1所遗留的锁无法释放的问题。

存在问题:

      1、释放别人的锁——>请求A已经获取到锁,在执行业务,但是还没有执行完成,过期时间到了,那么该锁就会被释放;此时请求B能够获取该锁,且执行业务逻辑,但是此时请求A执行完成需要释放锁,但是此时释放的锁是请求B的,也就是释放别人的锁。

      2、业务还没有执行完毕,但是锁已到过期时间

解决方法:

      1、释放别人的锁解决方法: 在上述获取锁的时候,我们仅单单设置了key值,但是value设置的为null,我们可以将上述版本优化为在拿取锁的时候同时设置key和value,将value设置为随机数,在释放锁的时候,先去判断一下该key的value值是否是之前设置的value值,是的话说明是自己的锁,进行释放,否则不是。

代码如下所示:

String productKey = "pro:detail:" + id;
    String productLockKey = "pro:detail:lock:" + id;
    //从缓存中获取数据
    String resultStr = redisUtil.get(productKey, String.class);
    //缓存中没有拿到数据,去数据库中拿取数据
    if (StringUtils.isEmpty(resultStr)) {

        /*存在问题:
            1、锁没有设置过期时间,那么当程序宕机时,将永远不会释放
            2、可能会删除别人的锁*/
        //进数据库获取数据
        String value = UUID.randomUUID().toString();
        try {
            //尝试加锁
            //boolean ifAbsent = redisUtil.setIfAbsent(key, null);
            boolean ifAbsent = redisUtil.setIfAbsent(productLockKey, value, 1000, TimeUnit.SECONDS);
            if (ifAbsent) {
                System.out.println("从数据库中获取数据......");
                resultStr = "商品详情:查询数据库";
                //拿到数据后去将该数据放入缓存中
                redisUtil.set(productKey, resultStr + "存入缓存中!");
            } else {
                Thread.sleep(5000);
                //开启自旋
                this.redisLock(id);
            }
        } finally {
            if (value.equals(redisUtil.get(productLockKey))) {
                //释放锁
                redisUtil.del(productLockKey);
            }

        }
    } else {
        resultStr = "商品详情:查询缓存";
    }
    return resultStr;
}

通过K V同时判断是否是自己的分布式锁,这样就避免了释放别人的锁的问题。
      2、业务没有执行完毕,但是时间已经过期
            这种问题可以采用写一个守护线程,然后每隔固定时间去查看redis锁是否过期,如果没有过期的话就延长其过期时间,也就是为其锁续期。
            上面也就是俗称了看门狗机制,上述逻辑已经有技术实现——Redission。
            下面我就详细讲解下Redission实现其分布式锁。
      

1.2.2、Redission实现分布式锁

redission实现分布式锁流程图:

分布式网站redis设计 redis做分布式_分布式锁_04

redission处理业务逻辑:

分布式网站redis设计 redis做分布式_分布式_05

代码如下所示:

//缓存中没有拿到数据,去数据库中拿取数据
if (StringUtils.isEmpty(resultStr)) {
    RLock lock = redisson.getLock(productLockKey);
    try {
        if (lock.tryLock(0, 5, TimeUnit.SECONDS)) {

            System.out.println("从数据库中获取数据......");
            resultStr = "商品详情:查询数据库";
            //拿到数据后去将该数据放入缓存中
            redisUtil.set(productKey, resultStr + "存入缓存中!");
        } else {
            Thread.sleep(5000);
            this.redissionLock(id);
        }
    } finally {
        //判断该lock是否已经锁
        if (lock.isLocked()) {
            //判断锁是否是自己的
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

    }
} else {
    resultStr = "商品详情:查询缓存";
}

      redission首先获取锁(get lock()),然后尝试加锁,加锁成功后可以执行下面的业务逻辑,执行完毕之后,会释放该分布式锁。
      redission解决了redis实现分布式锁中出现的锁过期问题,还有释放他人的锁:它的内部机制是默认锁过期时间是30s,然后会有一个定时任务在每10s去扫描一下该锁是否被释放,如果没有释放那么就延长至30s,这个机制就是看门狗机制。
      如果请求没有获取到锁,那么它将while循环获取继续尝试加锁。
      redission实际还存在一个问题,就是当redis是主从架构时,线程A刚刚成功的加锁在了master节点,还没有同步到slave节点,此时master节点给挂了,然后线程B这时过来是可以加锁的,但是实际上它已经加锁过了,这就是所出现的问题,这个问题涉及了高一致性 ,也就是C原则了;redission是无法解决高一致性问题的。
      如果想要解决高一致性可以使用红锁,或者zk锁;他们保证了高一致性,但是不建议使用,因为为了保证高一致性,它丢失了高可用性,对用户体验感不好,且出现上述问题出现几率不大,不能因为这种很小的问题出现几率而舍弃其高可用性。
      这里我也就不过多的对这两种锁来做具体的讲述,大家如果有兴趣的话可以自己找找文章,他们的原理无非就是必须多节点加锁成功才算加锁成功。
      好了,今天的技术就给大家聊到这,大家有那些问题不懂的和我在上述文章中不对的内容,欢迎大家给我在下面指出,帮助别人就是帮助自己。加油。