分布式锁-Redis

一、为什么要有锁的概念

1.假如现在我们有这么一个场景:

用户在淘宝app上购买商品,用户提交订单的时候提交了,多点击了几次。

不管用户点击几次,只要用户一直停留在一个页面,那么就必须生成一个订单。

1.1 如果我们的服务是单体服务的话

在这里插入图片描述

比如现在我们的并发量,单体服务不够我们来支撑了,那么我们得升级到微服务,这个时候我们的大致流程图就变成这样了。

1.2 微服务的话

在这里插入图片描述

二、分布式锁的实现方式

分布式锁必须满足的特性:

  • 互斥性
  • 防死锁
  • 自己解自己的锁。不能解其他人的锁
  • 容错性(防止节点故障。)

1.Redis 实现分布式锁

1.2 SETNX命令

使用Redis完成分布式锁之前,首先我们的Redis必须有 互斥 的能力,我们可以使用 SETNX 命令,这个命令表示 SET IF NOT EXISTS,
如果key不存在,我们就设置他的值。如果存在我们什么也不做。

我们使用Redis客户端来演示一下:

  1. 客户端1加锁:
    在这里插入图片描述
    加锁成功返回的是1
    2.客户端2加锁:
    在这里插入图片描述
    返回的是0,代表我们加锁失败了
    此时,加锁成功的客户端,就可以去操作我们的数据库了,可以提交订单添加数据了。操作完成后。我们执行Redis的 Del命令删除掉这个Key,也就是我们释放锁资源。
    3.释放锁资源
    在这里插入图片描述

其中这样会导致一些问题。不知道大家想到了没有
假如说我们的客户端1拿到锁资源后,如果发生了下面的场景,就会导致这个锁资源一直被它占用着

  • 程序处理业务逻辑异常,没及时释放锁资源
  • 进程挂了,没机会事放锁

这就导致 死锁
其实解决这个问题很简单,我们看下Redis里面的这个命令。

1.3 SETEX命令

在Redis种实现时,就是我们添加这个KEY的时候 我们给他加一个过期时间。
假设我们这里操作共享资源的时间不超过10s,那我们加锁时这么添加
在这里插入图片描述
这样一来,无论客户端是否异常,这个锁都可以在10s后被【自动释放】,其他客户端就可以拿到锁资源。

但是现在还是有问题:
现在的操作,加锁、设置过期时2条命令,有没有可能只执行了第一条命令,第二条却 来不及 执行呢?例如

  • SETNX 执行成功,执行EXPIRE时由于忘了问题,执行失败
  • SETNX 执行成功,Redis异常宕机,EXPIRE没有计汇执行
  • SETNX 执行成功,客户端异常崩溃,EXPIRE也没有机会执行

总之,这两条命令不能保证时原子性操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生 死锁 的问题

在Redis 2.6.12之后,Redis扩展了SET命令的参数,我们使用这个命令就可以了。

SET lock 1 EX 10 NX

但是这样的话,其实我们这还要一个问题。比如
我A加了锁,我在执行我的逻辑
然后B线程来了,给我把我的锁给我干掉了 这样不就乱套了么

因为我们上面写 删除锁的时候,我们并没有去判断这个锁,是不是我们线程A的锁,所以就会发生释放别人锁的风险,这样的解决流程,非常不 严谨,那么如何解决这个问题呢?

1.4 释放了别人的锁怎么办?

其实我们可以这样写,比如我们添加锁的时候设置
set 锁资源名称 当前线程的唯一标识 EX 过期时间 NX

SET lock $uuid EX 20 NX

然后我们释放锁资源的时候,判断当前key的value是不是我们当前线程所持有的,如果是就进行删除,那我们的命令是不是可以这么写

//如果get到的lock的值 == 我们的线程持有的值
if redis.get("lock") == $uuid:
	//那么我们就进行删除操作
    redis.del("lock")

这里释放锁使用的是 GET+DEL 两条命令,这时,又会遇到我们前面讲的原子性,那么怎么解决呢?

1.5 使用Lua脚本来保证原子性和解锁?

我们可以使用Lua脚结合get和del一起保证原子性

//安全释放锁的Lua脚本如下:
if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

1.6 使用Jedis 实现分布式锁

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Component
public class RedisLock {


    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);

    @Autowired
    private JedisPool jedisPool;

    private final static String RELEASE_LOCK_LUA =
            "if redis.call('get',KEYS[1])==ARGV[1] then\n" +
                    "        return redis.call('del', KEYS[1])\n" +
                    "    else return 0 end";

    //续锁逻辑:判断是持有锁的线程才能续锁
    private final static String DELAY_LOCK_LUA =
            "if redis.call('get',KEYS[1])==ARGV[1] then\n" +
                    "        return redis.call('pexpire', KEYS[1],ARGV[2])\n" +
                    "    else return 0 end";

    /**
     * 加锁代码
     *
     * @param key     key值
     * @param timeout 过期时间
     * @param value   value值
     * @return true成功 false失败
     */
    public boolean tryLock(String key, String value, Long timeout, TimeUnit timeUnit) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //将传递过来的时间 转成毫秒
            long toMillis = timeUnit.toMillis(timeout);
            SetParams params = new SetParams();
            params.px(toMillis);
            params.nx();
            if ("OK".equals(jedis.set(key, value, params))) {
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            throw new RuntimeException("分布式锁尝试加锁失败!");
        } finally {
            jedis.close();
        }
    }


    /**
     * 添加分布式锁 + 看门狗代码
     * <p>
     * 如果发现分布式锁 快到期了 并且业务逻辑 还没也释放锁 就给锁添加过期时间
     *
     * @param key
     * @param timeout
     * @param value
     * @return
     */
    public boolean lock(String key, String value, Long timeout, TimeUnit timeUnit) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            long toMillis = timeUnit.toMillis(timeout);
            SetParams params = new SetParams();
            params.px(toMillis);
            params.nx();
            if ("OK".equals(jedis.set(key, value, params))) {
                this.lockWait(key, toMillis, value);
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            throw new RuntimeException("分布式锁尝试加锁失败!");
        } finally {
            jedis.close();
        }
    }


    /**
     * 如果加锁成功 开启一个守护线程 判断这个锁 有没有被释放 如果没有被释放的话 延续这个锁的时间
     */
    public void lockWait(String key, Long timeout, String value) {
        scheduledExecutorService.schedule(() -> {
            //判断这个key 是否存在 存在就续期 否则 不虚其
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                Long result = (Long) jedis.eval(DELAY_LOCK_LUA,
                        Arrays.asList(key),
                        Arrays.asList(value, String.valueOf(timeout)));
                if (result.longValue() == 0L) {
                    System.out.println("Redis上的锁已释放,无需续期!");
                } else {
                    this.lockWait(key, timeout, value);
                    System.out.println("Redis锁无人释放,正在续期!");
                }
            } catch (Exception e) {
                throw new RuntimeException("锁续期失败!", e);
            } finally {
                if (jedis != null) jedis.close();
            }

        }, timeout - 100, TimeUnit.MILLISECONDS);

    }


    /**
     * 释放所资源
     *
     * @param key
     * @param value
     */
    public void unlock(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Long result = (Long) jedis.eval(RELEASE_LOCK_LUA,
                    Arrays.asList(key),
                    Arrays.asList(value));
            if (result.longValue() != 0L) {
                System.out.println("Redis上的锁已释放!");
            } else {
                System.out.println("Redis上的锁释放失败!");
            }
        } catch (Exception e) {
            throw new RuntimeException("释放锁失败!", e);
        } finally {
            if (jedis != null) jedis.close();
            System.out.println("本地锁所有权已释放!");
        }
    }

}

  • 看门狗不是setnx,而是expire 命令去加失效时间,所以这里不会产生死锁,只要你业务线程释放锁了,或者是说业务线程挂了(看门狗是业务线程的守护线程也会挂,所以特不会续锁),都会导致锁过期,所以就不会死锁。
  • 看门狗机制的代码,就是判断当前锁的资源 是不是9秒了有没有人在处理(如果你加锁的TTL是10秒,就设定9秒),这里要加一个判断,如果是锁存在,也就是key还存在,则续上10秒的TLL,如果没有就不处理,认为业务线程已经释放锁了

1.7 使用Redission实现分布式锁

redission中两个加锁的方法

    /**
     * 该方法尝试在指定的等待时间内获取锁,并且设置锁的租期
     * <p>
     * 如果锁当前不可用,方法会等待一段时间(waitTime)直到获取到锁,或者在等待期间线程被中断抛出 InterruptedException
     * <p>
     * 如果在等待期间获取到锁,则返回 true
     * <p>
     * 如果在等待期间未能获取到锁(超过等待时间),则返回 false。
     * <p>
     * 获取到锁后,锁会自动在指定的租期时间(leaseTime)后释放。
     *
     * @param waitTime  参数指定了尝试获取锁的最大等待时间
     * @param leaseTime 参数指定了锁的租期时间
     * @param unit      参数指定了 waitTime 和 leaseTime 的时间单位
     * @return
     * @throws InterruptedException
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;


    /**
     * 该方法用于获取锁,并指定锁的租期时间
     * <p>
     * 调用该方法后,如果锁当前不可用,则当前线程将被阻塞,直到成功获取到锁。
     * <p>
     * 当调用 lock 方法时,如果锁当前不可用,则当前线程将一直等待,直到获取到锁。
     * <p>
     * 获取到锁后,锁将在指定的租期时间(leaseTime)后自动释放。
     * <p>
     * 如果将 leaseTime 参数设置为 -1,表示持有锁直到显式调用 unlock 方法来释放锁。
     * <p>
     * 使用 lock 方法可以确保当前线程获取到锁后,其他线程在此期间将被阻塞。该方法适用于需要确保获取到锁才能继续执行的场景,以及需要指定锁的租期时间的场景
     *
     * @param leaseTime 参数指定了锁的租期时间
     * @param unit      参数指定了 leaseTime 的时间单位。
     */
    void lock(long leaseTime, TimeUnit unit);

释放锁的方法都是

    /**
     * 该方法用于释放锁
     * <p>
     * 调用 unlock 方法将释放之前获取的锁,使其变为可用状态。
     * <p>
     * 只有持有锁的线程才能成功调用 unlock 方法释放锁。
     * <p>
     * unlock 方法没有返回值,它只是用于显式释放锁。
     * <p>
     * unlock 方法在 Redission 中用于释放之前获取的锁资源,以便其他线程能够获取到该锁并执行相应的任务。
     */
    void unlock();

1.8 Redis集群模式

Redis集群模式(主从复制的架构下) 也可以使用我们上面的这种方式来用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

往日时光--

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值