Redis实战(2)——互斥命令用于构建分布式锁

1 什么是分布式锁

在单体应用中,线程锁是可以让多个线程串行执行一段代码逻辑的。不过在集群环境或者是分布式的环境下,线程锁无法保证线程串行运行,从而出现线程安全的问题。

根本的原因在于,在 集群分布式环境下 \textcolor{red}{集群分布式环境下} 集群分布式环境下,用于确保线程串行运行的线程监视器有多个。因为服务如果是分布式的部署,那么一定是在多个JVM中运行的。每个JVM中都将维护自己的堆栈空间。线程监视器同样如此。每个线程监视器都有可能被线程键入, 所以集群、分布式环境下线程锁无线确保线程安全 \textcolor{blue}{所以集群、分布式环境下线程锁无线确保线程安全} 所以集群、分布式环境下线程锁无线确保线程安全

在这里插入图片描述

所以在这种情况下,需要使用分布式锁。

在这里插入图片描述
分布式锁是在集群或者分布式环境下,多进程【线程】可见且互斥的锁。具有以下几个特点。

  • 高可用的获取锁与释放锁;
  • 高性能的获取锁与释放锁;
  • 具备锁失效机制,防止死锁;
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
2 Redis互斥指令实现分布式锁

实现分布式锁有多种方案,如关系型数据库。可在数据库表中维护一张业务锁信息。但是其性能受到数据库性能的影响。而redis数据库是基于内存的数据库,且可集群部署,所以满足高性能和高可用。在互斥与可见方面,可利用setnx这种互斥指令来实现。
另外为了避免服务宕机导致死锁的问题,可以利用redis 的key ttl 时间,超时时可自动删除,防止死锁。总的来说。选用Redis数据库来实现分布式锁是一个有效的方案。
在这里插入图片描述
从该流程可看出,redis实现分布式锁分为两步:获得锁和释放锁。初步实现如下

public class DistributeLock {

    private StringRedisTemplate stringRedisTemplate;
    //具体业务实现锁时,构造函数传入StringRedisTemplate 操作redis数据库
    public DistributeLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    /**
     * 获得分布式锁 [基于不同的业务]
     * @param serviceName 业务名称
     * @param time 安全机制 设置过期时间,避免服务宕机时出现死锁问题
     * @param unit 过期时间单位
     * @return
     */
    public boolean getLock(String serviceName, long time, TimeUnit unit){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        //set nx ex指令。set key成功表示获得锁成功
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, key, time, unit);
        //避免拆箱时 空指针问题
        return Boolean.TRUE.equals(aBoolean);

    }
    /**
     * 释放分布式锁  [基于不同的业务]
     * @param serviceName 业务名称
     * @return
     */
    public void delLock(String serviceName ){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        //释放锁
        stringRedisTemplate.delete(key);
    }

2.1 Redis分布式锁超时被误删除问题

上述分布式锁是否完美了呢。显然是可以待优化的。因为上面的锁实现会出现业务超时,线程误删除锁的问题。线程时序图如下
在这里插入图片描述

由于在获得锁的时候,设置key 的过期时间。如果在执行业务时阻塞超时,甚至超过了设置的key过期时间,在业务还没执行完成时,由于redis的过期机制,锁自动就释放了。那么其他的线程就可以获得这个锁。同样的当前一个线程业务执行完成后,在释放锁的时候可能会释放其他线程持有的锁,这样会出现在集群环境下分布式锁带来的线程安全问题。

这里主要问题就是线程会将其他线程持有的锁给删除掉。这里可以在获得锁的时候存入线程标识,删除前判定线程标识是否为当前持有锁的线程。减少锁误删的问题。优化锁的逻辑如下:
在这里插入图片描述

public class DistributeLock {

    private StringRedisTemplate stringRedisTemplate;

    public DistributeLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+":";

    /**
     * 获得分布式锁 [基于不同的业务]
     * @param serviceName 业务名称
     * @param time 安全机制 设置过期时间,避免服务宕机时出现死锁问题
     * @param unit 过期时间单位
     * @return
     */
    public boolean getLock(String serviceName, long time, TimeUnit unit){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        long threadId=Thread.currentThread().getId();
        //线程标识 线程标识为UUID+线程ID 的组合
        String threadMark=ID_PREFIX+threadId;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, threadMark, time, unit);
        //避免拆箱时 空指针问题
        return Boolean.TRUE.equals(aBoolean);

    }
    /**
     * 释放分布式锁  [基于不同的业务]
     * @param serviceName 业务名称
     * @return
     */
    public void delLock(String serviceName ){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        //获得上锁时存入的线程标识信息
        String value=stringRedisTemplate.opsForValue().get(key);
        //判断是否与存入的线程标识一致
        String myThreadMark=ID_PREFIX+Thread.currentThread().getName();
        if(value.equals(myThreadMark)){
            //标识一致时,释放线程的锁,避免误释放其他的锁
            stringRedisTemplate.delete(key);
        }
    }
}

2.2 Redis分布式锁因为非原子性被误删除

优化后,还会出现线程不安全的问题吗。在某些极端情况下,还是会出现线程锁误删,线程不安全的情形。分析时序图如下:

在这里插入图片描述

因为线程一在业务完成后删除锁的阶段中,在判断属于自己的锁后准备删除锁时发生了阻塞,导致原本持有的锁释放。线程二从而抢到锁执行业务逻辑,不过在此阶段中线程一抢到了时间片,继续执行删除锁的逻辑,从而删除了本该线程二持有的锁。此后线程三抢到锁,出现了线程二和线程三并行执行的情况,线程不安全。
经过分析可知,出现锁误删的源头在于,极端情形下,判断锁与删除锁的步骤不是原子操作的,那么就有可能出现线程并行的问题。这里采用的解决方案是在删除锁时将多个命令写在Lua脚本中,然后通过redis 执行Lua脚本。因为lua脚本能保证命令执行的原子性。

删除锁的 L u a 脚本通常可放在 x x x . l u a 文件中,如下所示为 u n l o c k . l u a ,该文件可放在 c l a s s p a t h 路径下: \textcolor{red}{删除锁的Lua脚本通常可放在xxx.lua文件中,如下所示为unlock.lua,该文件可放在classpath 路径下:} 删除锁的Lua脚本通常可放在xxx.lua文件中,如下所示为unlock.lua,该文件可放在classpath路径下:

-- 释放锁的lua脚本
-- 成功释放返回1,没有释放返回0
-- 判断线程锁标识是否一致
if(redis.call('get','KEYS[1]')==ARGV[1])
    --一致则删除锁数据
then redis.call('del',KEYS[1])
end
return 0

优化代码如下:

public class DistributeLock {

    private StringRedisTemplate stringRedisTemplate;

    public DistributeLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+":";

    //释放锁的一个脚本
    private static DefaultRedisScript<Integer> redisScript;

    //初始化删除锁脚本,避免每次调用Lua脚本都要去读取IO产生的延迟问题,
    // static静态代码块只加载一次
    static {
        redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Integer.class);//返回类型是Int
        //lua文件存放在resources目录下的redis文件夹内
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/unlock.lua")));
    }


    /**
     * 获得分布式锁 [基于不同的业务]
     * @param serviceName 业务名称
     * @param time 安全机制 设置过期时间,避免服务宕机时出现死锁问题
     * @param unit 过期时间单位
     * @return
     */
    public boolean getLock(String serviceName, long time, TimeUnit unit){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        long threadId=Thread.currentThread().getId();
        //线程标识
        String threadMark=ID_PREFIX+threadId;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, threadMark, time, unit);
        //避免拆箱时 空指针问题
        return Boolean.TRUE.equals(aBoolean);

    }
    /**
     * 释放分布式锁  [基于不同的业务]
     * @param serviceName 业务名称
     * @return
     */
    public void delLock(String serviceName ){
       //执行LUA脚本
        //泛型T为脚本返回的类型
        //<T> T execute(RedisScript<T> var1, List<K> var2, Object... var3);
        //var1lua 脚本
        //var2 keys
        //vars value 信息
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        String myThreadMark=ID_PREFIX+Thread.currentThread().getName();
        Integer result = stringRedisTemplate.execute(redisScript, Arrays.asList(key), myThreadMark);
    }


}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值