iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > JAVA >Redis实现分布式锁
  • 552
分享到

Redis实现分布式锁

分布式javajvm 2023-08-16 19:08:55 552人浏览 八月长安
摘要

单体锁存在的问题 在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。 (下述实例是一个简单的下单问题:从redis中获取库存,检查库存是否够,>0才允许下单) 我们的解决办法通常是加锁。如下加单体锁

单体存在的问题

在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。

(下述实例是一个简单的下单问题:从redis中获取库存,检查库存是否够,>0才允许下单)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KfYkawiq-1658890675878)(network-img/image-20220707110433029.png)]

我们的解决办法通常是加锁。如下加单体锁(synchronized或RentranLock)来保证单个实例并发安全

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fwyM0u8u-1658890675879)(network-img/image-20220707110831335.png)]

但上锁代码块内线程只能串行执行,效率低。单体应用难以满足实际高并发访问需求,会将单体应用部署到多个Tomcat实例上,由负载均衡将请求分发到不同实例上。

一个tomocat实例是一个JVM进程,单体锁(synchronized、ReentrantLock)是JVM层面的锁,只能控制单个实例上的并发访问安全,多实例下依然存在数据一致性问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bMPaf3G0-1658890675879)(network-img/image-20220726225915630.png)]

分布式

分布式锁登场:分布式锁指的是,所有服务中的所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁

分布式锁是可以跨越多个实例,多个进程的锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yr73zC0O-1658890675879)(network-img/image-20220726230619124.png)]

分布式锁具备的条件

互斥性:任意时刻,只能有一个客户端持有锁

锁超时释放:持有锁超时,可以释放,防止死锁

可重入性:一个线程获取了锁之后,可以再次对其请求加锁

高可用、高性能:加锁和解锁开销要尽可能低,同时保证高可用

安全性:锁只能被持有该锁的服务(或应用)释放。

容错性:在持有锁的服务崩溃时,锁仍能得到释放,避免死锁。

分布式锁实现方案

分布式锁都是通过第三方组件来实现的,目前比较流行的分布式锁的解决方案有:

数据库,通过数据库可以实现分布式锁,但是在高并发的情况下对数据库压力较大,所以很少使用。
2、Redis,借助Redis也可以实现分布式锁,而且Redis的Java客户端种类很多,使用的方法也不尽相同。
3、ZooKeeper,Zookeeper也可以实现分布式锁,同样Zookeeper也存在多个Java客户端,使用方法也不相同

(数据库、Zookeeper实现可参考:https://blog.csdn.net/poizxc2014/article/details/123963250,这里主要介绍哈redis实现分布式锁)

Redis实现分布式锁

再回到上述获取库存的实例,使用Redis实现并发访问安全。

1)基本方案:Redis提供了setXX指令来实现分布式锁

SETNX

格式: setnx key value
将key 的值设为value ,当且仅当key不存在。
若给定的 key已经存在,则SETNX不做任何动作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g4U8bhPS-1658890675880)(network-img/image-20220707145936215.png)]

设置分布式锁后,能保证并发安全,但上述代码还存在问题,如果执行过程中出现异常,程序就直接抛出异常退出,导致锁没有释放造成最终死锁的问题。(即使将锁放在finally中释放,但是假如是执行到中途系统宕机,锁还是没有被成功的释放掉,依然会出现死锁现象)

2)方案改进:可以给锁设置一个超时时间,到时自动释放锁(锁的过期时间大于业务执行时间)

在这里插入图片描述

上述两行代码中,由于加锁和设置锁过期时间不是原子的,可能加锁完就宕机了,那死锁依然存在,所以需要保证两指令执行的原子性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VbT1ifyx-1658890675880)(network-img/image-20220707121049879.png)]

连起来一起写可以原子执行。

3)改进三:再看看是否还有问题。假设有多个线程,锁的过期时间10s,线程1上锁后执行业务逻辑的时长超过十秒,锁到期释放锁,线程2就可以获得锁执行,此时线程1执行完删除锁,删除的就是线程2持有的锁,线程3又可以获取锁,线程2执行完删除锁,删除的是线程3的锁,如此往后,这样就会出问题。

在这里插入图片描述

解决办法就是让线程只能删除自己的锁,即给每个线程上的锁添加唯一标识(这里UUID实现,基本不会出现重复),删除锁时判断这个标识:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OA91mg5Z-1658890675881)(network-img/image-20220707150306865.png)]

但上述红框中由于判定和释放锁不是原子的,极端情况下,可能判定可以释放锁,在执行删除锁操作前刚好时间到了,其他线程获取锁执行,前者线程删除锁删除的依然是别的线程的锁,所以要让删除锁具有原子性,可以利用redis事务lua脚本实现原子操作判断+删除

//redis事务或lua脚本(lua脚本的执行是原子的),如下

    @RequestMapping(" /deduct_stock")    public String deductStock() {        String REDIS_LOCK = "Good_lock";        // 每个人进来先要进行加锁,key值为"good_lock"        String value = UUID.randomUUID().toString().replace("-","");        try{            // 为key加一个过期时间            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);            // 加锁失败            if(!flag){                return "抢锁失败!";            }            System.out.println( value+ " 抢锁成功");            String result = template.opsForValue().get("goods:001");            int total = result == null ? 0 : Integer.parseInt(result);            if (total > 0) {                // 如果在此处需要调用其他微服务,处理时间较长。。。                int realTotal = total - 1;                template.opsForValue().set("goods:001", String.valueOf(realTotal));                System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8002");                return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8002";            } else {                System.out.println("购买商品失败,服务端口为8002");            }            return "购买商品失败,服务端口为8002";        }finally {            // 谁加的锁,谁才能删除            // 也可以使用redis事务            // Https://redis.io/commands/set            // 使用Lua脚本,进行锁的删除            Jedis jedis = null;            try{                jedis = RedisUtils.getJedis();                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +                        "then " +                        "return redis.call('del',KEYS[1]) " +                        "else " +                        "   return 0 " +                        "end";                Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));                if("1".equals(eval.toString())){                    System.out.println("-----del redis lock ok....");                }else{                    System.out.println("-----del redis lock error ....");                }            }catch (Exception e){            }finally {                if(null != jedis){                    jedis.close();                }            }            // redis事务//            while(true){//                template.watch(REDIS_LOCK);//                if(template.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){//                    template.setEnableTransactionSupport(true);//                    template.multi();//                    template.delete(REDIS_LOCK);//                    List list = template.exec();//                    if(list == null){//                        continue;//                    }//                }//                template.unwatch();//                break;//            }        }            }} 

4)当然,也有不错的框架解决该问题,如Redission,Redisson是redis官网推荐实现分布式锁的一个第三方类库,通过开启另一个服务,后台进程定时检查持有锁的线程是否继续持有锁了,是将锁的生命周期重置到指定时间,即防止线程释放锁之前过期,所以将锁声明周期通过重置延长)

如下,先引入依赖,并在在主启动类中加入如下配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kkfNsqR5-1658890675881)(network-img/image-20220707144511286.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHaLu8dr-1658890675881)(network-img/image-20220707014151012.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wOXu5DjC-1658890675882)(network-img/image-20220707151420053.png)]

Redission执行流程如下:(只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下(锁续命周期就是设置的超时时间的三分之一),如果线程还持有锁,就会不断的延长锁key的生存时间。因此,Redis就是使用Redisson解决了锁过期释放,业务没执行完问题。当业务执行完,释放锁后,再关闭守护线程,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jgMaSCQd-1658890675882)(network-img/image-20220707014528848.png)
其他未获取锁的线程一直自旋。

前面几种方案都只是基于单机版的讨论,但是生产环境中如果是单机服务挂了,缓存就挂了,还不是很完美。为了实现高可用redis会集群部署

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CJ5w5gkj-1658890675882)(network-img/b8d53236d0d91be7d033ff0963f5b3e7.png)]

但在这种情况下又会出现锁丢失问题。

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,这就出现线程A还没执行完,线程B又来执行了,就会有并发安全问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYv4HUeF-1658890675883)(network-img/image-20220707022451171.png)]

为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。redlock是一种基于多节点redis实现分布式锁的算法,可以有效解决redis单点故障的问题。官方建议搭建五台redis服务器对redlock算法进行实现。

Redlock原理:搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。当超过半数的redis节点加锁成功才算成功获取锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vo7obWhO-1658890675883)(network-img/image-20220727103307642.png)]

RedLock的实现步骤:如下

获取当前时间,以毫秒为单位。
2.使用相同的key,value按顺序向5个master节点请求加锁,客户端设置网络连接和响应超时时间,并且设置获取锁的时间要远远小于锁自动释放的时间。假设锁自动释放时间是10秒,则获取时间应在5-50毫秒之间。通过这种方式避免客户端长时间等待一个已经关闭的实例,如果一个实例不可用了,则立即尝试获取下一个实例。
3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。使用的时间小于锁失效时间时,避免拿到一个已经过期的锁,并且要有超过半数的redis实例成功获取到锁,才算最终获取锁成功。如果不是超过半数,有可能出现多个客户端重复获取到锁,导致锁失效。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
4.

如果取到了锁,key的正失效时间应该为:过期时间-第三步的差值。
如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。

为了保证更高效的获取锁,还可以设置重试策略,在一定时间后重新尝试获取锁,但不能是无休止的,要设置重试次数。
简化下步骤就是:

按顺序向5个master节点请求加锁
根据设置的超时时间来判断,是不是要跳过该master节点。
如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!
Redisson实现了redLock版本的锁,有兴趣的小伙伴,可以去了解一下哈~

因为已经有半数的redis加锁了,假如有第二个线程来加锁,没加锁的redis少于一半,这样就没法加锁成功,从而保证并发安全

虽然通过redlock能够更加有效的防止redis单点问题,但是仍然是存在隐患的。假设redis没有开启持久化,clientA获取锁后,所有redis故障重启,则会导致clientA锁记录消失,clientB仍然能够获取到锁。这种情况虽然发生几率极低,但并不能保证肯定不会发生。

或者假设redis1,2都加锁成功并返回,redis3加锁成功返回,但是还没来得及同步就挂了,从节点变主节点,此时又有一个线程来加锁,reids3,4,5都set成功,那该线程也获取了锁,这样又会出问题。

保证的方案就是开始AOF持久化,但是要注意同步的策略,使用每秒同步,如果在一秒内重启,仍然数据丢失。使用always又会造成性能急剧下降。

官方推荐使用默认的AOF策略即每秒同步,且在redis停掉后,要在ttl时间后再重启。 缺点就是ttl时间内redis无法对外提供服务。

Redis与zookeeper分布式锁对比

RedLock实现分布式锁方式类似Zookeeper实现的分布式锁,zookeeper节点分布是树形结构,当某线程加锁向zookeeper中写入key后并不会立即返回结果,而是至少半数以上的从节点同步成功key后,才会返回上锁成功的结果,如果主节点挂了,他会确保选举出的从节点key一定存在。和redlock相比,虽然性能比redis实现慢了点,但是能确保系统安全

Redis与zookeeper分布式锁对比

  • redis集群是AP,zookeeper集群是CP,redis在集群架构上性能很高,zookeeper在数据一致性上做的更好。

  • redis往主节点写成功key,马上告诉客户端写成功,收到半数的返回结果就可以执行代码逻辑。所以redis采用抢占式方式进行锁的获取,需要不断的在用户态进行CAS尝试获取锁,对CPU占用率高。

  • zookeeper在主节点中写好key,会把key同步给所有从节点,并且接受从节点是否同步成功返回。当主节点接收到半数以上的同步成功的结果,就会返回客户端可以执行业务逻辑了。

  • Zookeeper不会出现主从架构锁丢失问题,主节点挂了,zookeeper的ZAB选举机制一定会把同步成功的从节点变为主节点。

  • 如果追求正确,就选zk集群,如果允许一点出错,追求性能,那选redis

  • 对于redis分布式锁的使用,在企业中是非常常见的,绝大多数情况不会出现极端情况。

redis实现分布式锁总结:

1)setnx:redis提供的分布式锁
存在问题:线程还没释放锁系统宕机了,造成死锁
2)setnx +setex:给锁设置过期时间,到期自动删除。
存在问题:因为加锁和过期时间设置非原子,存在设置超时时间失败情况,导致死锁
3)set(key,value,nx,px):将setnx+setex变成原子操作
存在问题:加锁和释放锁不是同一个线程的问题。假如线程1业务还没执行完,锁过期释放,线程2获取锁执行,线程1执行完业务删除锁删除的就是线程2的,然后其他线程又可获取锁执行,线程2执行完释放锁删除的是别人的,如此往复,导致并发安全问题。
4.方法1:在value中存入uuid(线程唯一标识),删除锁时判断该标识,同时删除锁需保证原子性,否则还是有删除别人锁问题,可通过lua或者redis事务释放锁
方法2:利用redis提供的第三方类库,Redisson也可解决任务超时,锁自动释放问题。其通过开启另一个服务,后台进程定时检查持有锁的线程是否继续持有锁了,是将锁的生命周期重置到指定时间,即防止线程释放锁之前过期,所以将锁声明周期通过重置延长。
Redission也可解决不可重入问题(AQS,计数)

问题:但上述方案能保证单机系统下的并发访问安全,实际为了保证redis高可用,redis一般会集群部署。单机解决方案会出现锁丢失问题。如线程set值后成功获取锁但主节点还没来得及同步就宕机了,从节点选举成为主节点,没有锁信息,此时其他线程就可以加锁成功,导致并发问题。

5)redis集群解决方案,使用redlock解决:

  • 顺序向5个节点请求加锁(5个节点相互独立,没任何关系)
  • 根据超时时间来判断是否要跳过该节点
  • 如果大于等于3节点加锁成功,并且使用时间小于锁有效期,则加锁成功,否则获取锁失败,解锁

参考文档:https://blog.csdn.net/poizxc2014/article/details/123963250

来源地址:https://blog.csdn.net/weixin_46129192/article/details/126010250

--结束END--

本文标题: Redis实现分布式锁

本文链接: https://www.lsjlt.com/news/372015.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Redis实现分布式锁
    单体锁存在的问题 在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。 (下述实例是一个简单的下单问题:从redis中获取库存,检查库存是否够,>0才允许下单) 我们的解决办法通常是加锁。如下加单体锁...
    99+
    2023-08-16
    分布式 java jvm
  • Redis——》实现分布式锁
    推荐链接:     总结——》【Java】     总结——》【Mysql】     总结——》【Redis】     总结——》【Kafka】     总结——》【Spring】     总结—...
    99+
    2023-09-03
    redis 分布式 过期 lua
  • PHP+Redis实现分布式锁
    目录 一、分布式锁概述 二、redis实现锁的命令 1、redis实现锁的命令 3、释放锁的步骤 三、PHP+redis分布式锁示例 四、redis集群分布式锁 一、分布式锁概述         在分布式环境下,各个线程通过对公共资...
    99+
    2023-09-15
    分布式锁
  • redis分布式锁的实现
    一、使用分布式锁要满足的几个条件:1、系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)2、共享资源(各个系统访问同一个资源,资源的载体可...
    99+
    2022-10-18
  • Redis Template实现分布式锁
    今天就跟大家聊聊有关Redis Template实现分布式锁,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。可靠性首先,为了确保分布式锁可用,我们至少...
    99+
    2022-10-18
  • python实现redis分布式锁
    #!/usr/bin/env python # coding=utf-8 import time import redis class RedisLock(object): def __init__(self, key): ...
    99+
    2023-01-31
    分布式 python redis
  • Redis实现分布式锁(SETNX)
    目录 1、什么是分布式锁 2、分布式锁应具备的条件         3、为什么使用分布式锁 4、SETNX介绍 5、分布式锁实现 6、效果演示 7、Redisson分布式锁详解 8、Lua脚本实现可重入分布式锁 1、什么是分布式锁  ...
    99+
    2023-10-21
    redis 分布式 java spring boot 后端
  • Redis分布式锁的实现方式
    目录一、分布式锁是什么1、获取锁2、释放锁二、代码实例上面代码存在锁误删问题:三、基于SETNX实现的分布式锁存在下面几个问题1、不可重入2、不可重试3、超时释放4、主从一致性四、Redisson实现分布式锁1、pom2...
    99+
    2023-04-03
    Java Redis分布式锁实现方式 实现Redis分布式锁 Redis分布式锁实现
  • Redis分布式锁之红锁的实现
    目录一、问题二、办法三、原理四、实战一、问题 分布式锁,当我们请求一个分布式锁的时候,成功了,但是这时候slave还没有复制我们的锁,masterDown了,我们的应用继续请求锁的时候,会从继任了master的原slav...
    99+
    2022-08-09
    Redis红锁
  • Redis如何实现分布式锁
    目录一、前言二、正文今天我们来聊一聊分布式锁的那些事。 相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行...
    99+
    2022-11-12
  • 基于Redis实现分布式锁
    我们知道分布式锁的特性是排他、避免死锁、高可用。分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是...
    99+
    2017-09-11
    基于Redis实现分布式锁
  • Redis分布式锁怎么实现
    这篇文章给大家分享的是有关Redis分布式锁怎么实现的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。分布式锁一般有三种实现方式:1、数据库乐观锁;2、基于Redis的分布式锁;3、...
    99+
    2022-10-18
  • Redis分布式锁如何实现
    这篇文章将为大家详细讲解有关Redis分布式锁如何实现,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。什么是分布式锁?要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、...
    99+
    2022-10-18
  • Redis分布式锁Redlock的实现
    目录普通实现Redlock实现Redlock源码用法唯一ID获取锁释放锁普通实现 说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value p...
    99+
    2022-11-12
  • Redis实现分布式锁详解
    目录一、前言为什么需要分布式锁?二、基于Redis实现分布式锁为什么redis可以实现分布式锁?如何实现?锁的获取锁的释放三、如何避免死锁?锁的过期时间如何设置?避免死锁锁过期处理释放其他服务的锁如何处理呢?那么redi...
    99+
    2023-04-09
    Redis实现分布式锁 Redis分布式锁
  • C#实现Redis的分布式锁
    目录Redis实现分布式锁(悲观锁/乐观锁)Redis连接池使用Redis的SetNX命令实现加锁,调用方式Redis实现分布式锁(悲观锁/乐观锁) 对锁的概念和应用场景在此就不阐...
    99+
    2022-11-12
  • Redis怎么实现分布式锁
    这篇文章主要介绍“Redis怎么实现分布式锁”,在日常操作中,相信很多人在Redis怎么实现分布式锁问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Redis怎么实现分布式锁”的疑惑有所帮助!接下来,请跟着小编...
    99+
    2023-06-02
  • redis实现分布式锁实例分析
    本文小编为大家详细介绍“redis实现分布式锁实例分析”,内容详细,步骤清晰,细节处理妥当,希望这篇“redis实现分布式锁实例分析”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。1、业务场景引入模拟一个电商系统,...
    99+
    2023-06-29
  • 分布式锁的原理及Redis怎么实现分布式锁
    这篇文章主要介绍“分布式锁的原理及Redis怎么实现分布式锁”,在日常操作中,相信很多人在分布式锁的原理及Redis怎么实现分布式锁问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解...
    99+
    2023-02-02
    redis
  • SpringBoot+Redis如何实现分布式锁
    这篇文章主要介绍了SpringBoot+Redis如何实现分布式锁,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。jedis的nx生成锁 如何删除锁 模拟抢...
    99+
    2023-06-16
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作