黑马点评学习笔记(分布式锁篇)

 1.分布式锁优化一人一单

本项目采用redis

        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

采用opsForValue的setIfAbsent方法,确保每次只有一个锁存在,同时增加过期时间,防止死锁。

public class SimpleRedisLock implements ILock {

    private final String name;
    private final StringRedisTemplate stringRedisTemplate;

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


    // 获取锁
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示,因为我们要区分到底是哪个线程拿到了这个锁,方便之后unLock的时候进行判断
        String 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() {
      //通过del删除锁
      stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

注意,在返回那里,返回的是Boolean.TRUE.equals(success);而不是直接返回success,这样不会返回空指针。

VoucherOrderServiceImpl   

@Override
    public Result seckillVoucher(Long voucherId) {
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        // 判断是否在秒杀时间内
      

        // 判断是否还有库存


        Long userId = UserHolder.getUser().getId();
        // 之前的:没有考虑集群模式下的锁问题


        // 【完善代码】:考虑集群模式下的锁问题
        //创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
        //加锁失败
        if (!isLock) {
            return Result.fail("之前的下单逻辑还在处理/不允许重复下单");
        }
        
        // 这里就是为了调用createVoucherOrder方法,但是要考虑到事务的问题,所以要通过代理对象来调用
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

关于调用代理:在类的内部直接调用另一个类内部的方法,不一定都需要使用代理。但如果你希望事务管理、AOP 切面(例如日志、权限检查、缓存等)或者其他基于代理的功能正常生效,就必须通过代理来调用这些方法。·

2.误删问题

 在线程1拿到业务后,执行业务时出现了堵,从而使锁超时释放,线程2 便挤了进来,拿到了线程1的锁,然后最后线程1完成后又提前把线程2拿到的锁给提前释放了,这种问题如何解决呢?

解决思路:在获取锁时存入线程标示(可以用UUID表示),一致放锁,不一致不放锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@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);
}

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

3.分布式锁的原子性问题

这里涉及到了一个很极端的情况。我们在上一节通过标识判断解决了误删问题,但是如果我们判断标识之后,还没有来得及释放锁,线程收到了堵塞,锁提前超时释放了。这时线程2又可以拿到锁了。然后线程1继续释放锁,把线程2的锁给释放了。此处的误删根本原因是锁的标识判断和释放锁的操作不是原子性的。

解决思路: Lua脚本解决多条命令原子性问题

RedisTemplate中可以利用execute方法去执行lua脚本。

-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    -- 一致,则删除锁
    return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
    // 提前在静态代码块中加载lua脚本,,提高性能
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 void unlock() {
     // 调用lua脚本解决误删问题,确保原子性
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            // 字符串转集合
            Collections.singletonList(KEY_PREFIX + name),
            // 线程标识
            ID_PREFIX + Thread.currentThread().getId());
}

 4.分布式锁Redission

我们基于setnx实现的锁有以下弊端:

关于主从节点

Redisson 是一个基于 Redis 的 Java 客户端,提供了丰富的功能扩展和高级功能,旨在通过分布式架构简化和增强 Redis 的使用。它在标准的 Redis 客户端基础上做了很多增强,提供了更为简洁、灵活的 API 以及支持更复杂分布式场景的能力。

Redission提供了分布式锁的多种多样的功能

4.Redission快速入门

5.Redission实现可重入锁的原理

Redission底层也是使用lua脚本实现的,此处不做过多阐述。

6.锁重试和WatchDog机制

Redisson 中,watchdog 是一个用于 自动续期 分布式锁和其他分布式数据结构的机制。它主要用于解决 分布式锁失效的问题,保证在锁持有过程中,锁的有效期不会因为某些原因而被过期,确保在锁持有的线程执行时间超过锁的过期时间时,锁不会被其他线程错误地释放。

自动续期:Redisson 的 watchdog 确保锁在超时之前会被自动续期,从而防止锁过期。特别是在长时间运行的操作中,避免了由于业务逻辑耗时过长,锁被其他客户端误释放的问题。

总结

7.MutiLock锁解决主从不一致问题

如果主节点宕机,会从从节点中选拔一个新的节点作为主节点。如果主从同步尚未完成,会出现锁失效的问题。

现在在所有主节点中都存放一份锁,要求一个线程必须从所有主节点中获取锁,才算真正获取锁。

假如此时有一个主节点宕机,恰好主从同步没有完成。若有其它线程趁虚而入获取到了新主节点的锁,但因为没能获取其它主节点的锁,因此也是获取锁失败的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值