redis--20.1--锁--分布式锁(自己实现)

redis–20.1–锁–分布式锁(自己实现)


1、分布式锁

目前JVM提供的锁(synchronized)只能作用到当前系统,跨系统是不支持锁操作的,这个时候就要使用分布式锁了。

1.1、分布式锁的几点考虑

1.1.1、互斥

同一时刻只能有一个线程获得锁

1.1.2、防止死锁

非正常原因导致代码无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式锁有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

1.1.3、性能

对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。

所以在锁的设计时,需要考虑两点。

  1. 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
  2. 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。

1.1.4、重入

我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。

1.2、怎么实现分布式锁?我们听过问题来阐述这个问题。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.3、使用场景

分布式环境,操作共享资源的情况

1.3.1、定时任务

集群环境下的定时任务,存在A服务器执行任务t1,B服务器也执行了任务t1,如果t1是创建订单,那么就会出现重复订单。
这个是否可以使用分布式锁来解决问题。

1.4、分布式锁原理

在这里插入图片描述

2、常见分布式锁方案对比

2.1、基于ZooKeeper(不推荐)

2.1.1、实现原理

  1. 加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。
    1. 若是,则表示获取到锁;
    2. 若否,则则watch /lock目录下序号比自身小的前一个节点
  2. 解锁:删除节点

2.1.2、优点

  1. 由zk保障系统高可用
  2. Curator框架已原生支持系列分布式锁命令,使用简单

2.1.3、缺点

维护一套zk集群,维保成本高

2.2、基于redis命令(不推荐)

2.2.1、实现原理

  1. 加锁:执行setnx,若成功再执行expire添加过期时间
  2. 解锁:执行delete命令

2.2.2、优点

实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好

2.2.3、缺点

  1. setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁
  2. delete命令存在误删除非当前线程持有的锁的可能
  3. 不支持阻塞等待、不可重入

2.3、基于redis Lua脚本(推荐)

2.3.1、实现原理

  1. 加锁
-- KEYS[1]为lock_name,ARGV[1]为random_value, ARGV[2]为seconds,  
redis.call('SET', KEYS[1]),ARGV[1]);
redis.call('pexpire', KEYS[1], ARGV[2]);
  
 
  1. 解锁:执行Lua脚本,释放锁时验证random_value
-- KEYS[1]为lock_name,ARGV[1]为random_value
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

2.3.2、优点

实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好

2.3.3、缺点

  1. 不支持阻塞等待、不可重入

3、代码实现

3.1、代码实现–不包含锁续命

3.1.1、代码

@RequestMapping("lockTest")
public void testLockLua(@RequestParam("proId") String proId) {

    // 当前商品id获取到锁(秒杀商品场景)
    String locKey = "lock:" + proId;
    // 获取锁
    Boolean lock = RedisUtil.lock(locKey, 3000, 2000000L);

    if (lock) {
        //获取缓存中的数字
        Object value = RedisUtil.get("num");
        // 如果是空,
        if (StringUtils.isEmpty(value)) {
            value = 0;
        }
        // 转int 类型
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入缓存
        RedisUtil.set("num", String.valueOf(++num));
        //当前的库存数
        System.out.println("当前线程id:" + Thread.currentThread().getId() + ",当前数字:" + num);
    }
    //分布式锁:释放锁
    RedisUtil.unlock(locKey);

}
/**
 * 分布式锁:获取锁
 *
 * @param lockKey    redis 的key
 * @param expireTime 过期时间,单位毫秒,过期时间是为了防止死锁
 * @param outTime    超时时间,单位毫秒,获取锁的超时时间,过了这个时间就不获取锁了,默认5秒
 * @return java.lang.Boolean
 * @author <a href="920786312@qq.com">周飞</a>
 * @since 2024/3/23 20:20
 */
public static Boolean lock(String lockKey, long expireTime, Long outTime) {

    if (outTime == null) {//如果超时时间没有值,设置默认值
        outTime = defaultOutTime;
    }
    // 声明一个lockValue,这个lockValue要保证唯一 ,用于保证加锁和解锁必须是同一个线程
    // 这里使用当前线程的id
    String lockValue = lockKeyPrefix + Thread.currentThread().getId();

    // 定义一个锁key前缀,防止key冲突
    lockKey = lockKeyPrefix + lockKey;

    long startTime = System.currentTimeMillis();//当前时间


    while (true) {//没有获取锁,不断尝试,超过超时时间,就不尝试了。
        // 获取锁, 这里的命令是: set locKey lockValue NX EX  3
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
        if (lock) {
            //开启一个异步线程,定时给锁续命
            return true;
        }
        //超过超时时间,就不尝试了。
        if (System.currentTimeMillis() - startTime > outTime) {
            break;//跳出循环
        }
        try {
            Thread.sleep(100); // 睡眠100毫秒后继续尝试
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return false;
}


/**
 * 分布式锁:释放锁
 *
 * @param lockKey
 * @return void
 * @author <a href="920786312@qq.com">周飞</a>
 * @since 2024/3/23 20:36
 */
public static void unlock(String lockKey) {
    // 定义一个锁key前缀,防止key冲突
    lockKey = lockKeyPrefix + lockKey;

    //线程id
    String lockValue = lockKeyPrefix + Thread.currentThread().getId();

    // 使用lua脚本来保证释放锁的原子性
    // 定义lua 脚本,格式如下
    // redis.call('命令名称','key类型的参数','其他参数'......)
    // key类型的参数:会放入KEYS数组
    // 其他参数:会放入ARGV数组
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    // 使用redis执行lua执行
    redisScript.setScriptText(script);

    // 设置一下返回值类型 为Long
    // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String类型,那么返回字符串0,会有发生错误。
    //
    redisScript.setResultType(Long.class);

    // 第1个参数:script脚本
    // 第2个参数:lockKey,对应lua脚本的 KEYS[1]
    // 第3个参数:lockValue,对应lua脚本的 ARGV[1]
    redisTemplate.execute(redisScript, Arrays.asList(lockKey), lockValue);

}

3.1.2、测试

ab -n 2000 -c 200 -k  http://192.168.43.45:8080/lockTest?proId=111

在这里插入图片描述

3.2、代码实现–包含锁续命

/**
     * 锁续命方法
     *
     * @param lockKey    redis 的key
     * @param expireTime 过期时间,单位毫秒,过期时间是为了防止死锁
     * @return void
     * @author <a href="920786312@qq.com">周飞</a>
     * @since 2024/3/24 18:00
     */
    public static void addLockLife(String lockKey, long expireTime) {
        AtomicInteger num = new AtomicInteger(0);//续命次数
        long initialDelay = expireTime / 3; //延迟执行时间
        long period = expireTime - initialDelay;//定时任务执行间隔时间,要小于过期时间
        ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
            if (!expire(lockKey, expireTime)) {//重置过期时间失败,说明key已经不存在,key不存在说明线程结束了
                //结束这个定时任务。
                logger.debug("分布式锁lock:lockKey:" + lockKey + ",结束续命,续命总次数" + num.get());
                futures.get(lockKey).cancel(false);
                futures.remove(lockKey);//从map中删除
            }
            logger.debug("分布式锁lock:lockKey:" + lockKey + ",锁续命次数" + num.incrementAndGet());
        }, initialDelay, period, TimeUnit.MILLISECONDS);//开始定时任务实现续命,每period毫秒续命一次
        futures.put(lockKey, scheduledFuture);

    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值