前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis 分布式锁(14)

Redis 分布式锁(14)

作者头像
兜兜毛毛
发布2021-05-18 10:56:10
5280
发布2021-05-18 10:56:10
举报
文章被收录于专栏:兜兜毛毛

什么是分布式锁

在分布式系统中,有些业务场景会用到分布式锁,实现分布式锁的方式有很多,本篇主要讲根据Redis如何来实现。

首先我们要知道分布式锁的一些基本特点:

  1. 互斥性:只有一个客户端可以持有锁
  2. 不会产生死锁:即使持有锁的客户端崩溃,也能保证后续其他客户端可以获得锁
  3. 只有持有这把锁的客户端才能解锁

下边我们通过几个例子来说明分布式锁为什么需要以上3个特点。

不加客户端校验解锁
代码语言:javascript
复制
/**
 * 使用jedis客户端实现分布式锁
 * @Author: maomao
 * @Date: 2021-04-27 08:42
 */
public class DistLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param value 值
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String value, String requestId, int expireTime) {
        // set支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 直接删除解锁,未判断客户端ID,会导致其他客户端把锁释放
     * @param jedis
     * @param lockKey
     */
    public static void releaseLock1(Jedis jedis, String lockKey) {
        jedis.del(lockKey);
    }
}

上边代码是一个不验证客户端的例子,加锁是没有问题的,但在解锁时会有很大的问题。

通过上图可以看到,因为没有校验客户端逻辑,Thread B可以直接解锁,而Thread A程序还未执行完,但已被解锁,造成锁失效。如果此时有其他客户端加锁是可以加锁成功的。

那我们可以在代码中增加一个客户端校验不就可以了?

加客户端校验解锁
代码语言:javascript
复制
   /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识-修改此处为客户端唯一标致
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

   /**
     * 增加锁判断,但因判断与删除不是原子操作,在并发场景时,会导致错误删除
     * @param jedis
     * @param lockKey
     * @param requestId
     */
    public static void releaseLock2(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时,这把锁突然不是这个客户端的,则会误解锁
            // 两个操作不能保证原子性
            jedis.del(lockKey);
        }
    }

在解锁代码中可以看到,我们也增加了客户端标志校验应该可以解决客户端校验问题了吧?其实并没有,我们要知道对redis来说,每个命令都是原子的,你的get与del方法是两个命令,无法保证原子操作。也就是我们多线程中常见的i++;操作,其实他是由3个操作执行。

那我们如何确保get与del的原子操作呢?我们可以使用lua脚本来实现。上述代码我们可以调整为一个lua脚本。

代码语言:javascript
复制
   /**
     * 释放分布式锁,使用lua脚本删除,可确保判断与删除的原子操作
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

通过增加客户端校验与解锁的原子性就可以实现安全的解锁。

有了上边的方式是不是就可以确保分布式锁的全部问题了?并不是,还有一种场景没有考虑到。

程序执行时间超出锁的过期时间

如果我们的加锁程序执行时间超出锁过期时间时,就会导致分布式锁失效。此时其他客户端是可以获得到锁的。如下图:

那么这种问题如何解决呢?

Redission

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),提供了分布式和可扩展的Java数据结构,比如分布式对象,分布式集合(Map、List、Queue、Set),分布式锁等等功能,不需要自己去运行一个服务实现。

Redisson官网

Redisson Git地址

Redission是由一个中国人与俄罗斯人共同发起的,所以中文文档比较详细。

使用Redission实现分布式锁

使用Redission可以很简单的实现分布式锁,代码如下:

代码语言:javascript
复制
public static void main(String[] args) throws InterruptedException {
        //设定锁标志
        //会在redis中创建一个Hash,Key是客户端UUID,value是锁重入次数
        RLock rLock = redissonClient.getLock("lockKey");
        // 最多等待100秒、上锁10s以后自动解锁
        if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
            System.out.println("获取锁成功,此时可以查看redis中的数据!");
        }
        //线程等待后可在redis中查到
        Thread.sleep(20000);
        rLock.unlock();
}

Redission不只可以实现独占锁,还可以实现如:可重入锁、公平锁、联锁、红锁、读写锁等等。

redission实现分布式锁的逻辑基本与上边我们讲的原理差不多,它还解决了我们最后一个问题,程序执行时间超出锁过期时间的问题。

他使用了一个《看门狗》的概念来实现自动续期。默认最大续期时间30s,也就是说如果业务超出30秒还未执行会自动解锁。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是分布式锁
    • 不加客户端校验解锁
      • 加客户端校验解锁
        • 程序执行时间超出锁的过期时间
        • Redission
          • 使用Redission实现分布式锁
          相关产品与服务
          云数据库 Redis®
          腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档