Redis实战案例15-基于Redis实现分布式锁

1. 初级版本

在这里插入图片描述

注意自动拆箱时的空指针异常

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String lockName;

    private static final String KEY_PREFIX = "lock:";

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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + lockName, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // 自动拆箱存在null异常,不要直接返回success,做一下判断
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + lockName);
    }
}

之前synchronized方法修改为

// 一人一单
// 用户id
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
    // 获取代理对象
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

// 一人一单
// 用户id
Long userId = UserHolder.getUser().getId();
// 创建锁对象(注意要拼接用户id,实现单个用户的一人一单,只有同一个id的请求打来时才要进行锁定)
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
// 获取锁(稍微设置久一点,避免测试时锁失效)
boolean isLock = lock.tryLock(1200);
if(!isLock){
    return Result.fail("一人只能抢一次哦~");
}
try {
    // 获取IVoucherOrderService的代理对象
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
} finally {
    lock.unlock();
}

2. 初级版本存在的问题(重点)

在这里插入图片描述

当线程1获取锁时,发生业务阻塞,阻塞时间甚至超过了锁的释放时间,这时线程1会失去锁资源;

在这里插入图片描述

此时,线程2可以获取锁资源,并执行自己的业务。此时线程1获得了某些资源之后不再阻塞,开始执行自己的业务,并且执行完之后会进行锁的释放操作;
上述情况出现时,就会触发安全问题,线程1会把线程2的锁给释放掉,线程3也可以拿到锁了,锁机制也就相当于是失效了;

在这里插入图片描述

解决办法:给锁加标识(有点像乐观锁),线程在释放锁时多进行一次判断

在这里插入图片描述

流程图进行相应修改

在这里插入图片描述

3. 代码实现优化版本

采用UUID是因为每个JVM都会维护线程id的递增数字,如果直接采用线程id可能出现线程id冲突,使用UUID在集群环境下更合适;

在这里插入图片描述

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String lockName;

    private static final String KEY_PREFIX = "lock:";

    // 参数true去掉UUID自动给的下划线
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String lockName) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识,拼接上对应的UUID
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + lockName, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // 自动拆箱存在null异常,不要直接返回success,做一下判断
        return Boolean.TRUE.equals(success);
    }

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

4. 上述代码的原子性问题

在使用分布式锁的情况下,当一个线程持有锁时,其他线程需要等待该锁被释放才能获取到它。
然而,垃圾回收的发生可能会导致线程在释放锁之前出现延迟或暂停。具体来说,当垃圾回收器运行时,它会检查和清理不再被引用的对象,回收内存资源。在这个过程中,所有线程的执行都会被暂停,称为“停顿时间”。从而影响其他线程的等待时间;
如果一个线程在持有锁的过程中发生了垃圾回收的停顿,它可能无法及时释放锁,从而延长了其他线程等待锁的时间。并且可能存在锁到期自己释放了的情况;

在这里插入图片描述
例如:当上述代码进行释放锁的操作时,已经做完判断标识是否一致的if语句之后,在准备释放锁之前发送了阻塞(GC回收阻塞)。此时锁到期自动释放,线程2从而获取到锁资源,而恰好线程1恢复开始执行业务,注意,此时的线程1是已经判断完了标识一致操作之后才阻塞的,所以线程1会直接进行释放锁的操作,这样线程2获取的锁会被线程1释放掉

// 判断标识是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + lockName);
        }
    }

所以要确保加锁和释放锁具有原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值