Redis 分布式锁实现

Redis 分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

特点:

  • 多线程可见
  • 互斥
  • 高可用
  • 高性能(高并发)
  • 安全性、可重入性、重试机制、锁超时自动续期等 …

加锁之后,对整个分布式集群都有效

  • 基于数据库
  • redis缓存:使用setnx上锁,使用del释放锁;设置过期时间,自动释放 set user 10 nx ex 120
  • zookeeper

实现基于分布式锁需要实现两个方法:
在这里插入图片描述

  • 获取锁

    确保只能有一个线程获取锁,确保添加锁和添加过期时间的原子性

    非阻塞:尝试一次,成功返回 true,失败返回 false

    set key name ex 10 nx #ex是设置超时时间,nx是互斥
    
  • 释放锁

    手动释放

    超时释放:获取锁时添加一个超时时间

    del key
    

Redis 分布式锁的初级版本

Lock 接口

public interface ILock {

    /**
     * 尝试获取锁
     *
     * @param timeoutSec 锁持有的超时时间,过期自动释放
     * @return true:成功/false:失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

Lock 实现:

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";

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

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

    @Override
    public void unlock() {
        //释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}
SimpleRedisLock lock = new SimpleRedisLock();
try {
    if(lock.tryLock(time)){
        //执行业务逻辑
    }
} finally {
    lock.unlock();
}

存在的问题:

  • 业务执行时间过长,导致锁超时,自动释放
  • 线程一,锁超时后,线程二又获取到锁,线程一执行完逻辑后,释放锁,此时释放的是线程二的锁

改进 Redis 分布式锁

  • 在获取锁时存入线程的标识(可以使用UUID)
  • 在释放锁时先获取锁中的标识,判断是否与当前的线程标识是否相等,是,则释放;不是,则不释放

在这里插入图片描述

public class SimpleRedisLock implements ILock {

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

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

    @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);
        if (threadId.equals(id)) {
            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

存在的问题:

unlock 中判断锁标识的操作和释放锁的操作不是原子操作,如果threadId.equals(id)判断成功之后,产生了阻塞(如:Full GC时),导致多线程安全问题,解决方法可以使用 Lua 脚本,保证以上操作的原子性

再次改进 Redis 分布式锁

Redis提供了 Lua 脚本功能,在脚本中编写多条命令,确保多条命令执行时的原子性

释放锁思路:

  • 获取锁中的线程标识
  • 判断是否与当前的标识一致
  • 如果一致则释放(删除)锁,否则什么都不做
-- 获取锁标识,是否与当前线程一致?
if(redis.call('get', KEYS[1]) == ARGV[1]) then
    -- 一致,删除
    return redis.call('del', KEYS[1])
end
-- 不一致,直接返回
return 0
public class SimpleRedisLock implements ILock {

    private String name;
    private 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<>();
        //加载 Lua 脚本
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

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

    @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());
    }
}

Redisson

基于上述setnx实现的分布式锁还存在以下问题

  • 不可重入:同一个线程无法获取同一把锁
  • 不可重试:获取锁只尝试一次就返回 false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但是如果业务执行时间过长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果 Redis 提供了主从集群,主从同步存在延迟,当主机宕机时,尚未同步至从节点,会出现安全问题

Redisson是一个在 Redis 基础实现分布式工具的集合,包括分布式锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值