利用Redis实现分布式锁

写在最前面

我在之前总结幂等性的时候,写过一种分布式锁的实现,可惜当时没有真正应用过,着实的心虚啊。正好这段时间对这部分实践了一下,也算是对之前填坑了。

分布式锁按照网上的结论,大致分为三种:1、数据库乐观锁; 2、基于Redis的分布式锁;3.、基于ZooKeeper的分布式锁;

关于乐观锁的实现其实在之前已经讲的很清楚了,有兴趣的移步:使用mysql乐观锁解决并发问题 。今天先简单总结下redis的实现方法,后面详细研究过ZooKeeper的实现原理后再具体说说ZooKeeper的实现。

为什么需要分布式锁?

在传统单体应用单机部署的情况下,可以使用Java并发相关的锁,如ReentrantLcok或synchronized进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统,渐渐的被部署在多机器多JVM上同时提供服务,这使得原单机部署情况下的并发控制锁策略失效了,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

分布式锁的实现条件

1、互斥性,和单体应用一样,要保证任意时刻,只能有一个客户端持有锁

2、可靠性,要保证系统的稳定性,不能产生死锁

3、一致性,要保证锁只能由加锁人解锁,不能产生A的加锁被B用户解锁的情况

Redis分布式锁的实现

Redis实现分布式锁不同的人可能有不同的实现逻辑,但是核心就是下面三个方法。

SETNX
SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
Expire
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
Delete
delete key
删除key

获取锁

首先讲一个目前网上应用最多的一种实现

实现思路:

1.获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁以免产生死锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

2.获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

    public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {
        try {
            // 定义 redis 对应key 的value值(uuid) 作用 释放锁 随机生成value,根据项目情况修改
            String identifierValue = UUID.randomUUID().toString();
            // 定义在获取锁之后的超时时间
            int expireLock = (int) (timeOut / 1000);// 以秒为单位
            // 定义在获取锁之前的超时时间
            //使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁
            // 使用循环方式重试的获取锁
            Long endTime = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < endTime) {
                // 获取锁
                // 使用setnx命令插入对应的redislockKey ,如果返回为1 成功获取锁
                if (jedis.setnx(lockKey, identifierValue) == 1) {
                    // 设置对应key的有效期
                    jedis.expire(lockKey, expireLock);
                    return identifierValue;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

这种实现方法也是目前应用最多的实现,我一直以为这确实是正确的。然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么还是会发生死锁的情况。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

当然这种情况Jedis的设计者也显然想到了,新版的Jedis可以同时set多个参数,具体实现如下:

实现思路:

基本上和原来的逻辑类似,只是将setnx和expire的操作合并为一步,改为使用新的set多参的方法。

set(final String key, final String value, final String nxxx, final String expx,final long time)

key和value自然不用多说。nxxx参数只可以传String 类型的NX(仅在不存在的情况下设置)和XX(和普通的set操作一样会做更新操作)两种。

expx是指到期时间单位,可传参数为EX (秒)和 PX (毫秒),time就是具体的过期时间了,单位为前面expx所指定的。

然后我们对上面的代码进行改造如下:

    /**
     * @param acquireTimeout
     *            在获取锁之前的超时时间
     * @param timeOut
     *            在获取锁之后的超时时间
     */
    public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {
        try {
            // 定义 redis 对应key 的value值(uuid) 作用 释放锁 随机生成value,根据项目情况修改
            String identifierValue = UUID.randomUUID().toString();
            // 定义在获取锁之前的超时时间
            //使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁
            // 使用循环方式重试的获取锁
            Long endTime = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < endTime) {
                // 获取锁
                // set使用NX参数的方式就等同于 setnx()方法,成功返回OK.PX以毫秒为单位
                if ("OK".equals(jedis.set(lockKey, lockKey, "NX", "PX", timeOut))) {
                    return identifierValue;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

好了,获取锁的操作基本上就上面这些,有同学可能要问,为什么不直接返回一个Boolean型的true或false呢?

正如我前面所说的,要保证解锁的一致性,所以就需要通过value值来保证解锁人就是加锁人,而不能直接返回true或false了。

下面在说下解锁的过程。

释放锁

还是先举一个错误的例子:

实现思路:

释放锁的时候,通过传入key和加锁时返回的value值,判断传入的value是否和key从redis中取出的相等。相等则证明解锁人就是加锁人,执行delete释放锁的操作。

    // 释放redis锁
    public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {
        try {
            // 如果该锁的id 等于identifierValue 是同一把锁情况才可以删除
            if (jedis.get(lockKey).equals(identifierValue)) {
                jedis.del(lockKey);
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

看着好像没啥问题哈。然而仔细想想又总感觉哪里不对。

如果在执行jedis.del(lockKey)操作之前,刚好锁的过期时间到了,而这个时候又有别的客户端取到了锁,我们在此时执行删除操作,不是又不符合一致性的要求了吗。

然后我们修改为下述方案:

修改后的代码为:

public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {
        try {
            String script = "if redis.call(‘get‘, KEYS[1]) == ARGV[1] then return redis.call(‘del‘, KEYS[1]) else return 0 end";
            Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifierValue));
            //0释放锁失败。1释放成功
            if (1 == result) {
                //如果你想返回删除成功还是失败,可以在这里返回
                System.out.println(result+"释放锁成功");
            }
            if (0 == result){
                System.out.println(result+"释放锁失败");
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

实现思路:

我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为identifierValue。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与identifierValue相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。

那么为什么执行eval()方法可以确保原子性?源于Redis的特性,因为Redis是单线程,在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

总结

本文对Redis实现分布式锁做了比较详细的总结。我个人也对上述代码做了实践检验。其实我在使用时,一直用的错误的案例。直到看到园友Ruthless的一篇文章才晓得稀疏平常的写法竟然漏洞百出,感谢博客园,感谢Ruthless。下一篇准备再研究研究ZooKeeper的实现。

最后附上Ruthless的原文链接:https://www.cnblogs.com/linjiqin/p/8003838.html

原文地址:https://www.cnblogs.com/laoyeye/p/10011695.html

时间: 2024-10-08 06:02:36

利用Redis实现分布式锁的相关文章

利用redis实现分布式锁 - waen - 博客园

原文:利用redis实现分布式锁 - waen - 博客园 利用数据库触发器实现定期自动增量更新缓存 原文地址:https://www.cnblogs.com/lonelyxmas/p/10434810.html

spring boot 利用redisson实现redis的分布式锁

原文:http://liaoke0123.iteye.com/blog/2375469 利用redis实现分布式锁,网上搜索的大部分是使用java jedis实现的. redis官方推荐的分布式锁实现为redisson http://ifeve.com/redis-lock/ 以下为spring boot实现分布式锁的步骤 项目pom中需要添加官方依赖 我是1.8JDK固为 Pom代码   <!-- redisson --> <dependency> <groupId>

利用多写Redis实现分布式锁原理与实现分析

在我写这篇文章的时候,其实我还是挺纠结的,因为我这个方案本身也是雕虫小技拿出来显眼肯定会被贻笑大方,但是我最终还是拿出来与大家分享,我本着学习的态度和精神,希望大家能够给与我指导和改进方案. 一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子: 场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能在某一个时刻会有二笔一样的单子同时到达系统后台. 场景二:在App中下订单的时候,点击确认之后,没反应,就又点击了几次.在这种情况下,如果无法保证该接口的幂

基于Redis实现分布式锁-Redisson使用及源码分析

在分布式场景下,有很多种情况都需要实现最终一致性.在设计远程上下文的领域事件的时候,为了保证最终一致性,在通过领域事件进行通讯的方式中,可以共享存储(领域模型和消息的持久化数据源),或者做全局XA事务(两阶段提交,数据源可分开),也可以借助消息中间件(消费者处理需要能幂等).通过Observer模式来发布领域事件可以提供很好的高并发性能,并且事件存储也能追溯更小粒度的事件数据,使各个应用系统拥有更好的自治性. 本文主要探讨另外一种实现分布式最终一致性的解决方案--采用分布式锁.基于分布式锁的解决

Redis实现分布式锁原理与实现分析

一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子: 场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能在某一个时刻会有二笔一样的单子同时到达系统后台. 场景二:在App中下订单的时候,点击确认之后,没反应,就又点击了几次.在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题. 在接收消息的时候,消息推送重复.如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大. 类似这种场景,我们有很多种方法,可以使用幂等操作,也

基于redis的分布式锁(不适合用于生产环境)

基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境,另一个是集群环境下的Redis锁实现.在介绍分布式锁的实现之前,先来了解下分布式锁的一些信息. 2 分布式锁 2.1 什么是分布式锁? 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥

基于redis的分布式锁实现

关于分布式锁 很久之前有讲过并发编程中的锁并发编程的锁机制:synchronized和lock.在单进程的系统中,当存在多个线程可以同时改变某个变量时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量.而同步的本质是通过锁来实现的.为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记.

程序员修神之路--redis做分布式锁可能不那么简单

菜哥,复联四上映了,要不要一起去看看? 又想骗我电影票,对不对? 呵呵,想去看了叫我呀 看来你工作不饱和呀 哪有,这两天我刚基于redis写了一个分布式锁,很简单 不管你基于什么做分布式锁,你觉得很简单吗?来来来 在计算机世界里,对于锁大家并不陌生,在现代所有的语言中几乎都提供了语言级别锁的实现,为什么我们的程序有时候会这么依赖锁呢?这个问题还是要从计算机的发展说起,随着计算机硬件的不断升级,多核cpu,多线程,多通道等技术把计算机的计算速度大幅度提升,原来同一时间只能执行一条cpu指令的时代已

基于redis的分布式锁的分析与实践

转:https://my.oschina.net/wnjustdoit/blog/1606215 前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了.乐观锁和悲观锁最根本的区别在于线程之间是否相互阻塞. 那么,本文主要来讨论基于redis的分布式锁算法问题. 从2.6.12版本开始,redis为SET命令增加了一系列选项(SET key value [EX seconds] [PX