Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis分布式锁

基于Redis的分布式锁及原子性问题(短视频开发)

作者:云豹科技-苏凌霄

短视频开发中,Redis分布式锁通过SETNX实现加锁与解锁,需设置超时时间避免死锁,为防止误删,释放锁时需判断线程身份,并用Lua脚本保证原子性,确保安全操作,本文给大家介绍基于Redis的分布式锁及原子性问题,感兴趣的朋友一起看看吧

短视频开发,基于Redis的分布式锁及原子性问题

用 Redis 实现分布式锁

主要应用到的是 SETNX key value命令(如果不存在,则设置)

主要要实现两个功能:

1、获取锁(设置一个 key)
2、释放锁 (删除 key)
基本思想是执行了 SETNX命令的线程获得锁,在完成操作后,需要删除 key,释放锁。

加锁:

@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

释放锁:

@Override
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 释放锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

可是这里会存在一个隐患——假设该线程发生阻塞(或者其他问题),一直不释放锁(删除 key)这可怎么办?

为了解决这个问题,我们需要为 key 设计一个超时时间,让它超时失效;但是这个超时时间的长短却不好确定:

1、设置过短,会导致其他线程提前获得锁,引发线程安全问题。
2、设置过长,线程需要额外等待。

锁的误删

 超时时间是一个非常不好把握的东西,因为业务线程的阻塞时间是不可预估的,在极端情况下,它总能阻塞到 lock 超时失效,正如上图中的线程1,锁超时释放了,导致线程2也进来了,这时候 lock 是 线程2的锁了(key 相同,value不同,value一般是线程唯一标识);假设这时候,线程1突然不阻塞了,它要释放锁,如果按照刚刚的代码逻辑的话,它会释放掉线程2的锁;线程2的锁被释放掉之后,又会导致其他线程进来(线程3),如此往复。。。

为了解决这个问题,需要在释放锁时多加一个判断,每个线程只释放自己的锁,不能释放别人的锁!

释放锁

@Override
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

原子性问题

刚刚我们谈论的释放锁的逻辑:

1、判断当前锁是当前线程的锁
2、当前线程释放锁
可以看到释放锁是分两步完成的,如果你是对并发比较有感觉的话,应该一下子就知道这里会存在问题了。

分步执行,并发问题!

假设 线程1 已经判断当前锁是它的锁了,正准备释放锁,可偏偏这时候它阻塞了(可能是 FULL GC 引起的),锁超时失效,线程2来加锁,这时候锁是线程2的了;可是如果线程1这时候醒过来,因为它已经执行了步骤1了的,所以这时候它会直接直接步骤2,释放锁(可是此时的锁不是线程1的了)

其实这就是一个原子性的问题,刚刚释放锁的两步应该是原子的,不可分的!

要使得其满足原子性,则需要在 Redis 中使用 Lua 脚本了。

引入 Lua 脚本保持原子性
lua 脚本:

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

Java 中调用执行:

public class SimpleRedisLock implements ILock {
​
    private String name;
    private StringRedisTemplate stringRedisTemplate;
​
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
​
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
​
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
​
    @Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}

到了目前为止,我们设计的 Redis 分布式锁已经是生产可用的,相对完善的分布式锁了。

以上就是短视频开发,基于Redis的分布式锁及原子性问题, 更多内容欢迎关注之后的文章

到此这篇关于短视频开发,基于Redis的分布式锁及原子性问题的文章就介绍到这了,更多相关Redis的分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文