基于Redis实现分布式锁——Java版本

基于Redis实现分布式锁------Java版本

定义分布式锁接口如下:

java 复制代码
public interface ILock {

    boolean tryLock(long timeoutSec);

    void unlock();
}

版本一

设定业务超时时间,到期自动解锁。缺点是超时时间不好估计,需要略大于业务执行的时间。当超时时间小于执行业务时间时,其他线程会拿到锁,而之前的线程执行完后又会解锁,变得混乱,导致线程安全问题。

java 复制代码
public class SimpleRedisLock implements ILock{

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX = "lock:";
    
    @Override
    public boolean tryLock(long timeoutSec) {
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, Thread.currentThread().getId() + "",
                timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

版本二

解锁时判断锁是否和自己假的锁标识一样,标识使用UUID+线程ID,标识一样才释放锁。每个线程都会创建一个SimpleLock,因此保证UUID不一样。

java 复制代码
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        String value = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value,
                timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    @Override
    public void unlock() {
        String value = ID_PREFIX + Thread.currentThread().getId();
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (value.equals(id)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

版本三

某个线程先判断锁是自己的,此时由于其他原因阻塞,比如Full GC,其他线程拿到锁,之前的线程再解锁,但是解的并不是自己的锁,导致线程安全问题。

需要保证这些操作的原子性。使用Lua脚本。

使用redis提供的函数call。key类型参数放入KEYS数组,其他参数放入ARGV数组,Lua中数组角标从1开始。Lua脚本如下。

lua 复制代码
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    return redis.call('del', KEYS[1])
end
return 0

Lua脚本放在resources文件夹下,在Java代码中调用StringRedisTemplate的execute方法执行Lua脚本。最后分布式锁代码为

java 复制代码
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        String value = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value,
                timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    @Override
    public void unlock() {
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}

Redisson

上述实现的分布式锁缺点为:

1、不可重入,同一线程不能对同一把锁多次加锁。

2、不可重试。

3、超时时间不好设置,有可能超时自动释放,虽然不会有误删,但是存在其他线程重新加锁。

4、主从复制的单点问题,主节点宕机导致从节点锁还没有同步。

这些功能属于拓展功能,要么出现概率低,要么可以不需要这样的需求。

Redisson包含分布式锁的成熟实现。

1、Redisson的可重入锁实现原理:

参考ReentrantLock原理,需要存储加锁次数。因此使用Redis中的Hash数据结构。key是锁名称,field是UUID+线程id,value是加锁次数。

2、Redisson的可重试锁和超时释放实现原理:

while持续在重试时间内重试,但不是一直重试,而是消息订阅和信号量,释放了再来重试。

超时释放使用了看门狗机制,每10秒钟续期30秒,无限续期,直到调用unLock方法。

3、Redisson解决主从一致性问题的原理:

去中心化,不要主从,每个节点都需要获取锁,使用了红锁算法。N个节点需要获取N/2+1个锁才能加锁成功。使用Multilock。

缺点是增加读写。

对每个节点都使用配置类把Bean加载到容器中。

java 复制代码
RLock lock1 = redissonClient.getLock("order");
RLock lock2 = redissonClient2.getLock("order");
RLock lock3 = redissonClient3.getLock("order");
RLock lock = redissonClient.getMultiLock(lock1, lock2, lock3);
相关推荐
没有bug.的程序员7 小时前
服务安全:内部服务如何防止“裸奔”?
java·网络安全·云原生安全·服务安全·零信任架构·微服务安全·内部鉴权
一线大码8 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
哈里谢顿8 小时前
redis常见问题分析
redis
weixin_440730508 小时前
java数组整理笔记
java·开发语言·笔记
weixin_425023008 小时前
Spring Boot 实用核心技巧汇总:日期格式化、线程管控、MCP服务、AOP进阶等
java·spring boot·后端
一线大码8 小时前
Java 8-25 各个版本新特性总结
java·后端
2501_906150568 小时前
私有部署问卷系统操作实战记录-DWSurvey
java·运维·服务器·spring·开源
MySQL实战8 小时前
Redis 7.0 新特性之maxmemory-clients:限制客户端内存总使用量
数据库·redis
better_liang9 小时前
每日Java面试场景题知识点之-TCP/IP协议栈与Socket编程
java·tcp/ip·计算机网络·网络编程·socket·面试题
niucloud-admin9 小时前
java服务端——controller控制器
java·开发语言