使用原生Redis命令实现分布式锁

 一、为什么需要分布式锁?

       传统单体/集群开发都是 Jvm 进程内的锁如:lock锁,synchronized锁,再比如cas原子类轻量级锁,但是对于跨 Jvm 进程以及跨机器,这种锁就不适合业务场景,会存在问题。并且JDK原生的锁可以让不同线程之间以互斥的方式来访问共享资源,但若想要在不同进程之间以互斥的方式来访问共享资源,JDK原生的锁就无能为力了(对于多线程程序,避免同时操作一个共享变量而产生数据问题,我们通常会使用一把锁来互斥以保证共享变量的正确性,其使用范围是在同一个进程中,如果换做是多个进程,需要同时操作一个共享资源,如何互斥呢?现在的业务应用通常是微服务架构,这也意味着一个应用会部署多个进程,例如:多个进程如果需要修改MySQL中的同一行记录,多个进程同时启动定时任务更新数据等等,为了避免操作乱序导致脏数据,此时就需要引入分布式锁了)。

        因此,想要实现分布式锁,须借助一个外部系统,所有进程都去这个系统上申请加锁。而这个外部系统,必须要有互斥能力,即:两个请求同时进来的时候,只能给一个进程加锁成功,另一个失败。这个外部系统可以是数据库,也可以是Redis或Zookeeper,但考虑到性能,我们通常会选择使用Redis或Zookeeper来实现。

二、Redis分布式锁如何实现?

    核心思想set ex px nx + 校验唯一随机值,再删除

        若实现分布式锁,必须要求Redis有互斥的能力。

Redis实现分布式锁的核心命令如下:

SETEX key value

       SETEX:SET IF NOT EXIST,如果指定的key不存在,则创建并为其设置值,然后返回状态码1;如果指定的key存在,则直接返回0。如果返回值为1,代表获得该锁;此时其他进程再次尝试创建时,由于key已经存在,则都会返回0,代表锁已经被占用。

// 1、加锁SETNX lock_key 1// 2、实现业务逻辑DO THINGS// 3、释放锁DEL lock_key

        当获得锁的进程处理完成业务后,再通过del命令将该key删除,其他进程就可以再次竞争性地进行创建,获得该锁。但是存在以下问题:

    1、程序处理步骤二:实现业务逻辑发生异常,没及时释放锁;

    2、进程挂了,没机会释放锁。

       以上情况会导致已经获得锁的客户端一直占用锁,其他客户端永远无法获取到锁,也即“死锁

三、如何解决死锁问题?

        最容易想到的方案是在申请锁时,在Redis中实现时,给锁设置一个过期时间,假设操作共享资源的时间不会超过10s,那么加锁时,给这个key设置10s过期即可。在Redis 2.6.12之后,Redis扩展了SET命令的参数,可以在SET的同时指定EXPIRE时间,这条操作是原子的,例如:以下命令是设置锁的过期时间为10秒。

命令:SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]例如:SET lock_key 1 EX 10 NX
  • EX seconds-设置指定的终止时间,以秒为单位。
  • PX milliseconds-设置指定的终止时间(以毫秒为单位)。
  • NX – 仅在不存在的情况下设置key。
  • XX – 仅设置key(如果已存在)。

但是,还有锁过期/释放了别人的锁问题:

    1、线程1加锁成功,开始操作共享资源;

    2、线程1操作共享资源耗时太久,超过了锁的过期时间,锁失效(锁被自动释放);

    3、线程2加锁成功,开始操作共享资源;

    4、线程1操作共享资源完成,在finally块中手动释放锁,但此时它释放的是客户端2的锁。

问题分析:

    1、锁过期问题评估操作共享资源的时间不准确导致的,若只是增大过期时间,只能缓解问题降低出现问题的概率,仍然无法彻底解决问题。原因在于客户端在拿到锁之后,在操作共享资源时,遇到的场景是很复杂的,既然是预估的时间,也只能是大致的计算,不可能覆盖所有导致耗时变长的场景。

    2、释放了别人的锁问题原因在于释放锁的操作并没有检查这把锁的归属,这样解锁不严谨。

四、如何避免锁被别人给释放?

        客户端在加锁时,设置一个只有自己才知道的唯一标识进去,例如:可以是自己的线程ID/UUID产生的值,之后在释放锁时,要先判断这把锁是否归自己持有,只有是自己的才能释放它。

4.1、使用Lua脚本

//释放锁 比较unique_value是否相等,避免误释放if redis.get("key") == unique_value(线程ID/UUID产生的值) then    return redis.del("key")

       GET + DEL两个命令需要使用Lua脚本,保证原子的执行。因为Redis处理每个请求是单线程执行的,在执行一个Lua脚本时其它请求必须等待,直到这个Lua脚本处理完成,这样一来GET+DEL之间就不会有其他命令执行了。

unlock.script脚本如下:

//Lua脚本语言,释放锁 比较unique_value是否相等,避免误释放if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end

      其中,KEYS[1]:lock_key,ARGV[1]:当前客户端的唯一标识,这两个值都是我们在执行 Lua脚本时作为参数传入的。

在java代码中的运用:

     /**     * 解锁脚本,原子操作     */    private static final String unlockScript =            "if redis.call("get",KEYS[1]) == ARGV[1]n"                    + "thenn"                    + "    return redis.call("del",KEYS[1])n"                    + "elsen"                    + "    return 0n"                    + "end";

使用:

   /**     * 功能描述:使用Lua脚本解锁     * @MethodName: unlock     * @MethodParam: [name, token]     * @Return: boolean     * @Author: yyalin     * @CreateDate: 2023/7/17 18:41     */    public boolean unlock(String name, String token) {        byte[][] keysAndArgs = new byte[2][];        keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8")); //lock_key        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8")); //token的值,也即唯一标识符        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();        RedisConnection conn = factory.getConnection();        try {            Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")),                    ReturnType.INTEGER, 1, keysAndArgs);            if(result!=null && result>0)                return true;        }finally {            RedisConnectionUtils.releaseConnection(conn, factory);        }        return false;    }

五、代码实现

5.1、RedisLock类

/** * 功能描述:redis的分布式锁:解决并发问题 * @Author: yyalin * @CreateDate: 2023/7/18 10:17 */@Repositorypublic class RedisLock {    /**     * 解锁脚本,原子操作     */    private static final String unlockScript =            "if redis.call("get",KEYS[1]) == ARGV[1]n"                    + "thenn"                    + "    return redis.call("del",KEYS[1])n"                    + "elsen"                    + "    return 0n"                    + "end";
    private StringRedisTemplate redisTemplate;
    //有参构造函数    public RedisLock(StringRedisTemplate redisTemplate) {        this.redisTemplate = redisTemplate;    }
    /**     * 加锁,有阻塞     * @param name key的值     * @param expire 过期时间     * @param timeout 加锁执行超时时间     * @return     */    public String getLock(String name, long expire, long timeout){        long startTime = System.currentTimeMillis(); //获取开始时间        String token;        //规定的时间内,循环获取有值的token        do{            token = tryGetLock(name, expire);  //获取秘钥Key            if(token == null) {                if((System.currentTimeMillis()-startTime) > (timeout-50))                    break;                try {                    Thread.sleep(50); //try 50毫秒 per sec milliseconds                } catch (InterruptedException e) {                    e.printStackTrace();                    return null;                }            }        }while(token==null);        return token;    }    /**     * 加锁,无阻塞     * @param name 设置key     * @param expire     * @return     */    public String tryGetLock(String name, long expire) {        //获取UUID值为value        String token = UUID.randomUUID().toString();        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();        RedisConnection conn = factory.getConnection();        try{            Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")),  //设置name为key                    token.getBytes(Charset.forName("UTF-8")),  //设置token为value                    Expiration.from(expire, TimeUnit.MILLISECONDS), //设置过期时间:MILLISECONDS毫秒                    RedisStringCommands.SetOption.SET_IF_ABSENT); //如果name不存在创建            if(result!=null && result)                return token;        }finally {            RedisConnectionUtils.releaseConnection(conn, factory);        }        return null;    }
    /**     * 功能描述:使用Lua脚本解锁     * @MethodName: unlock     * @MethodParam: [name, token]     * @Return: boolean     * @Author: yyalin     * @CreateDate: 2023/7/17 18:41     */    public boolean unlock(String name, String token) {        byte[][] keysAndArgs = new byte[2][];        keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8")); //lock_key        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8")); //token的值,也即唯一标识符        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();        RedisConnection conn = factory.getConnection();        try {            Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")),                    ReturnType.INTEGER, 1, keysAndArgs);            if(result!=null && result>0)                return true;        }finally {            RedisConnectionUtils.releaseConnection(conn, factory);        }        return false;    }}

5.2、控制层调用

@ApiOperation(value="添加学生", notes="add")    @PostMapping("/add")    public void addStudent() throws InterruptedException {        String token = null;        try{            //设置锁并获取唯一值            token = redisLock.getLock("lock_name", 10*1000, 11*1000);            if(token != null) {                System.out.println("我拿到了锁哦:"+token);                // 开始执行业务代码                Thread.sleep(3*1000L);            } else {                System.out.println("我没有拿到锁唉");                //1000毫秒后过一会在尝试重新获取锁                Thread.sleep(5*1000L);                System.out.println("我开始重试来了。。。。。");                addStudent();            }        }finally {            if(token!=null) {                //用完进行释放锁                redisLock.unlock("lock_name", token);            }        }    }

六、总结

   基于Redis实现的分布式锁,一个严谨的流程如下:(set ex px nx + 校验唯一随机值,再删除)

    1、加锁时要设置过期时间SET lock_key unique_value EX expire_time NX

    2、操作共享资源(业务代码);

    3、释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁。

      到此原生的redis实现分布式加锁、解锁流程就更加严谨了,可以满足大部分场景,用来解决大部分的并发问题。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=14819,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?