分布式锁--redis实现

目录

1. 基本原理

2. redis实现

2.1 申请锁实现

2.2 释放锁实现

2.3 业务修改

2.4 reids分布式锁误删问题

2.4.1 逻辑说明

2.4.2 解决方案

2.4.3 代码修改

2.5 分布式锁的原子问题

2.5.1 逻辑说明

 2.5.2 解决方案

2.5.3 代码修改 

2.6 总结

3. 完整代码


1. 基本原理

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

        分布式锁的核心思想就是让大家使用同一把锁,这样就可实现一把锁锁住线程,这就是分布式的核心思路。(多个jvm共用一把锁)

分布式锁的特性:

  1. 可见性:多个线程都看到相同的结果
  2. 互斥性:互斥是分布式锁的基本特性,使程序串行执行
  3. 高性能性:由于加锁本身就让性能降低,因此对于分布式本身需要他有较高的加锁性能和释放锁的性能。
  4. 安全性:安全也是程序中不可缺少的。

2. redis实现

实现分布式锁使需要实现两个基本方法获取锁释放锁

核心思路:

利用redis的setNx方法,当多个线程进入时,第一个线程就获取到锁,此时redis中存在相应的key,返回1,其他线程要想去申请锁时,此时由于reids中已经存在了key,此时返回0,申请锁失败,进入线程等待。实现了互斥性。

2.1 申请锁实现

利用setnx方法进行加锁,同时增加过期时间,防止死锁。

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

2.2 释放锁实现

就是简单的删除操作

public void unlock() {
    //通过del删除锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

2.3 业务修改

    public Result addSeckillVoucherOrder2(Long voucherId) {
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //判断秒杀活动是否开启
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            return Result.fail("秒杀尚未开始");
        }
        //判断秒杀活动是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            return Result.fail("秒杀已经结束");
        }
        //查询库存不足
        if (voucher.getStock()<1){
            return Result.fail("库存不足");
        }
        //设置分布式锁,一人一订单。
        Long userId = UserHolder.getUser().getId();
        //在redis中设置互斥锁。
        SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
        //申请锁
        boolean isLock = lock.tryLock();
        if (!isLock){
            return Result.fail("不允许重复下单");
        }
        //申请成功,进行下单业务
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.seckillVoucher(voucherId);
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

2.4 reids分布式锁误删问题

2.4.1 逻辑说明

        在持有锁的线程(线程1)在执行过程中出现阻塞,导致锁过期释放,此时又来了其他线程,此时锁时空闲的,(线程2)申请锁成功,等到线程1业务阻塞完成,释放锁,但此时释放的确实线程2的锁,这就是误删

2.4.2 解决方案

        在加锁时,填入当前线程的标识,释放锁时,判断是否是当前线程的锁,如果是就释放,否则不做处理

2.4.3 代码修改

申请锁

 线程标识由线程id+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);
    }
}

2.5 分布式锁的原子问题

2.5.1 逻辑说明

        线程1在持有锁后,在准备释放锁并已经通过判断线程标识后,此时恰好进入阻塞。等到锁过期,线程2此时申请锁成功,此时线程1阻塞完成,继续进行释放锁(已经通过标识判断)的逻辑,这是删除的就是线程2的锁。这就是因为此时分布式锁并不满足原子性。

 2.5.2 解决方案

        reids提供了Lua脚本功能,在一个脚本中编写多条reids命令,确保多条命令执行时的原子性。基本语法可以参考网站:Lua 教程 | 菜鸟教程 。

这里介绍redis提供的调用函数,语法如下:

redis.call('命令名称', 'key', '其它参数', ...)

释放锁的业务逻辑:

  1. 获取锁中的线程标识
  2. 判断是否与指定的标识是否一致
  3. 如果一致释放锁
  4. 不一致什么都不做

lua脚本:

unlock.lua:

-- KEYS[1]就是锁的key值,ARGV[1]就是当前线程的标识
-- 判断标识是否一致
if(redis.call('GET',KEYS[1])==ARGV[1]) then
    -- 一致删除锁
    return redis.call('DEL',KEYS[1])
end
return 0;

2.5.3 代码修改 

利用java代码调用Lua脚本改造分布式锁

在redisTemplate中,可以利用execute方法去执行lua脚本

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 void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,//lua脚本初始化
            Collections.singletonList(KEY_PREFIX + name),//keys
            ID_PREFIX + Thread.currentThread().getId());//argv
}

2.6 总结

基于redis实现分布式锁:

  • 利用set nx ex 获取锁,并设置过期时间,保存线程标识
  • 释放锁时先判断线程标识是否与自己一致,一致则删除锁

此时,基于redis实现分布式锁已经实现,但还有一些问题时redis锁无法解决的,锁不住问题、不可重入和无法重试获取问题。要解决这些问题就需要使用redission来解决。

3. 完整代码

SimpleRedisLock

public class SimpleRedisLock implements ILock{
    private final StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock";
    private final String name;
    private static final String ID_PREFIX = UUID.randomUUID().toString()+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        //脚本初始化
        UNLOCK_SCRIPT = new DefaultRedisScript<Long>();
        //脚本文件位置
        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 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();
        //执行lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name),threadId);
    }

}

Lua脚本

保存在resource目录下

-- KEYS[1]就是锁的key值,ARGV[1]就是当前线程的标识
-- 判断标识是否一致
if(redis.call('GET',KEYS[1])==ARGV[1]) then
    -- 一致删除锁
    return redis.call('DEL',KEYS[1])
end
return 0;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值