Redis分布式锁

  • 集群架构下的并发问题
  • 分布式锁的实现原理和不同方式的实现对比
  • 基于Redis实现的分布式锁
  • Redis分布式锁1.0版
  • 基于Redis分布式锁1.0版的误删问题
  • 解决误删问题,Redis分布式锁2.0版
  • 基于Redis分布式锁2.0版的原子性问题
  • 解决原子性问题(Lua),Redis分布式锁3.0版
    • 使用Lua语言先编写逻辑代码
    • 使用Java(RedisTemplate)调用Lua脚本
  • 总结

集群架构下的并发问题

在单体架构上,乐观锁和悲观锁可以锁住并发情况下的同步代码块,我们多使用synchronized来对方法加锁。但是在配上负载均衡的集群模式下,普通的synchronized是无法锁住从两台服务器同时进入的请求
    这是在了解秒杀项目的难点之一:一人一单的并发安全问题在使用集群架构出现的难点。我们先从单体项目出发,单体项目很好理解,假如有俩线程:线程1查询订单,判断是否存在。然后第二个线程之后在去查询订单,判断是否存在,在synchronized的作用下两个线程是不会发生问题的。

那现在,我们不再是一台服务器,而是多台。在当前这一个JVM内部,锁的原理是在JVM内部维护了一个锁监视器对象,监视器的对象用的是userId,它是在我们的常量池里面。那么在这个JVM内部是维护了这一个常量池子,当ID相同的情况下,他们永远都是同一个锁,也就是说锁的监视器是同一个。所以无论是线程1也好,线程2也好,他们俩要获取锁的时候,锁监视器就会记录线程ID,当另一个线程再来获取锁的时候肯定是不行的,因为锁监视器已经记录一个了。

但是,当我们部署一个新的服务器的时候,也就是部署了一个新的JVM。两个JVM也拥有各自的常量池,JVM2用userId作为锁的时候,它的监视器对象就会拥有一个新的锁监视器,跟JVM1的监视器不是同一个。现在当我们线程3来获取锁的时候走的是自己的监视器,那这个监视器显示的是空的呀,所以也能获取锁成功,当然了线程4失败是没问题的。也就是说在JVM内部锁监视器能保证这些线程互斥,但是多个JVM就会有多个JVM监视器, 有多少个锁监视器就会有多少个线程成功进入同步代码块。

所以我们要解决的问题就是在多个JVM的情况下让这些锁监视器使用同一把锁。

分布式锁的实现原理和不同方式的实现对比

synchronized就是利用JVM内部的锁监视器来控制线程的,在JVM内部,因为只有一个锁监视器,所以只会有一个线程获取到锁,可以实现线程阶段互斥。但是当有多个JVM的时候,就会有多个锁监视器,这时候synchronized就会显得苍白无力,JVM内部的锁监视器直接作废。所以锁的监视器一定要在JVM的外部,让所有JVM都去找独一无二的锁监视器来获取锁,这样也就只有一个线程获取锁,也就实现了多JVM的线程互斥。

所以满足在分布式系统或集群模式下多线程可见并且互斥的锁就是分布式锁。

分布式锁核心是实现多进程之间的互斥,而满足这一点的方式有很多,常见的有三种:Mysql、Redis。Redis里有setnx互斥命令,王redis面set数据的时候,只有没数据的时候才会set成功,有数据就会set失败。

基于Redis实现的分布式锁

实现分布式锁肯定要实现两个基本方法,获取锁释放锁

获取锁

  • 互斥条件:确保只能有一个线程获取到锁。
  • 非阻塞:尝试一次,成功返回true,失败返回false。
    这个我们可以用redis的setnx,这个可以确保只有一个可以返回1。


    确保原子性,利用EX。

释放锁

  • 手动释放 del lock

    思考问题:如果在获取锁后redis宕机了,那么这个释放锁动作就永远得不到执行,其他线程进不来,我这服务已经挂了,整个线程进入死锁状态。所以需要在获取锁时添加过期时间,避免服务器宕机引起的死锁。

Redis分布式锁1.0版


锁的名称不能写死,不同的业务有不同的锁。

基于Redis分布式锁1.0版的误删问题

现在的Redis锁处于正常的工作状态,假如现在来了一个线程1,它想要获取到setnx,那么作为第一个的线程是肯定可以获取成功的。然后拿到锁后线程1就开始执行他的业务,其他线程想要获取锁就去阻塞状态等待。
    但是由于某种原因,线程1的业务产生了阻塞,那么这样的话它锁的持有周期就会变长,如果这个阻塞时间过长甚至超过了我们设置的最大超时时间,那么这个时候也会触发锁的超时释放。总的来说就是业务没完,超时时间将锁释放
    那么释放后其他线程就会趁机而入,现在线程2来获取,获取成功,然后开始执行它的业务。万万不巧的是,线程1的业务也阻塞完成了,开始了它的释放,这个删锁的过程也很巧的把线程2刚拿到的锁给释放了
    那么姗姗来迟的线程3页理所应当的拿到了锁开始了它的业务。此时此刻,有两个线程同时进入了锁,线程安全问题有可能再次发生。
    这个问题的本质就是由于业务阻塞导致锁提前释放, 等待线程1醒来后删掉的不是自己的锁而是线程2的锁。

解决误删问题,Redis分布式锁2.0版

其实自信分析误删问题,就会发现他们只需要在释放锁的时候验证当前的锁id是不是自己线程的id,所以我们在加锁的时候加上Thread的id即可。

  1. 在获取锁时存入线程标示(用UUID表示
  2. 在释放锁时先获取锁的线程标示,判断是否与当前线程一致。


    为了确保不会出现两个相同版本号的线程,UUID还是很不错的一个方法,这里用了之后可以转化为String类型然后再去掉横线。最后在UUID后面拼接线程ID即可,完美的保证了线程ID的唯一性。

在释放锁的时候就要去先做判断,步骤为获取线程标示→获取锁标示→判断是否一致(释放/跳过)

基于Redis分布式锁2.0版的原子性问题

就在我们刚更新完Redis2.0后,万万没想到,新的问题又出来了了。是这样的,Redis也是正常的工作状态,现在来了一个线程1,他来获取锁然后执行业务,这段业务非常流程,没有阻塞的过程,所以在执行完代码后就开始了它的释放锁操作
    而要释放锁就要先判断锁标示,这个判断也是没有问题的,因为锁就是他自己的。紧接着他就要执行释放锁的动作,但是万万没想到,就在要释放时,产生了阻塞,就在我纳闷我也没写与操作系统内核态交互的代码啊,为什么就阻塞了呢?我的日志告诉了我,我们JVM刚很不巧的执行了FULL GC,无论是CMS垃圾回收器还是G1垃圾回收器都有独自标记和独自清除的两端STW,所以很不巧,就在我代码执行到释放锁的时候,JVM为了回收垃圾自己进入了阻塞…
    所以接下来发生的事不用想就知道了,线程1又触发了超时时间,锁自己释放。珊珊来迟的线程2又不巧的获取到锁,就在准备执行业务代码的时候,线程1又醒了,他很坚定的删了以为是自己的锁,但那个锁确实线程2的… 然后线程2与线程3又开始互相撕逼起来,线程安全问题又来了…qwq

解决原子性问题(Lua),Redis分布式锁3.0版

说道原子性,想到的相比都是事务,那redis有没有事务呢?肯定是有的,不够这个事务跟我们所了解的mysql事务ACID是有很大差别的。redis的事务首先是能够保证原子性的,但是无法保证事务的一致性,而且redis事务的多个操作其实是一个批处理,实在最终一次性执行。那也就是说我们没办法先查询判断、最后在释放锁,因为做这些动作是拿不到结果的,他是最终一次性执行,所以是没办法把他们俩放到一个事务中,只能利用redis的乐观锁做一些判断,来确保在释放的时候没有人来进行修改,但是这样做会复杂很多,所以这里可以利用乐观锁来维护原子性,但是会拉低性能,很麻烦。
    这里推荐使用Lua脚本来实现。Redis是提供Lua脚本功能的,Lua脚本其实就是在一个脚本中编写多条Redis命令,确保命令执行时的原子性。可以参考这个网页来简单学习下如何维护原子性的操作https://www.runoob.com/lua/lua-tutorial.html
    其他语法不需要学习,只需要会用redis提供的调用函数:redis.call('命令名称','key','其他参数',...),比如我们要执行set name Jack,则脚本是这样:

先执行set、在执行get、最后用变量来接收:

写好脚本后,需要用Redis命令来调用脚本。

比如执行redis.call(‘set’,‘name’,‘jack’),语法为:

参数的设置很重要,如果不想写死,基本上set后面的两个值都需要传参,而且可能还不止set一个命令。所以我们可以设置数组,key类型的参数会放入KEYS数组,其他参数放入ARGV数组。Lua语言的数组从1开始而不是从0开始。

所以在原子性与一致性的保障下,我们释放锁的流程就变成了:

  1. 获取锁的线程标示
  2. 判断是否与指定标示(当前线程的标示)一致。
  3. 如果一致则释放锁
  4. 如果不一致则什么都不做

使用Lua语言先编写逻辑代码

先使用Lua脚本编写下逻辑代码

-- 锁的key 先写死
local key = "lock:order:5"-- 当前线程的标示 格式是UUID-线程id
local threadId = "asdwiwahsjdwa-33";-- 获取锁的线程标示
local id = redis.call('get',key)--比较线程标示与锁的标示是否一致
if(id == threadId) then--释放锁return redis.call('del',key)
end
return 0

因为我们的key和线程的id是存放到KEYS和ARGV数组里面的,所以定义的步骤可以取消,直接在获取标示的时候传参来表示。简化后就是:

-- 先取,然后进行比较
-- 这里的KEYS[1]就是锁的key,ARGV[1]就是当前的线程标示。-- 获取锁的线程标示
local id = redis.call('get',KEYS[1])--比较线程标示与锁的标示是否一致
if(id == ARGV[1]) then--释放锁return redis.call('del',KEYS[1])
end
return 0

使用Java(RedisTemplate)调用Lua脚本

@Override
public <T> T excute(RedisScript<T> script,List<T> keys,Object... args){return scriptExcutor.excute(script,keys,args);
}

在编写代码的时候,一定要先在resources里存放lua,然后再java代码里调用,不要在java代码里编写lua。

scriptExcutor.excute(script,keys,args)三个参数,就是脚本,key,arg… 。RedisScript是个类,我们写的Lua是个文件,显然这个类就要加载文件。所以我们就得提前吧文件读取好,所以我们要在全局变量中定义。使用RedisScript的实现类DefaultRedisScript类定义,调用他的setLocation()来设置脚本的位置。最后可以给它定义返回值的类型,使用setReturnType()

所以在使用起来就很方便了,第一个参数就传UNLOCK_SCRIPT 脚本
    第二个参数就是key的集合(也就是key数组吧,key[1]),就是我们定义的“lock:+业务名称”。很简单,使用Collections工具类提供的singletionList();字符串转集合的方法就行。
    第三参数就是args就更简单了,就是我们定义的格式UUID+ThreadId,之前已经定义过全局变量了。直接调用就行了。

然后也就不需要返回值了吧,因为你线程id不等于锁的id本来就是当前线程出现问题了,当前线程作废就行。

总结

现在的话,只有当前线程能删自己获得的锁,原子性也得到了解决。锁已经达到了生产可用的标准了。

总结思路:

  • 利用set nx ex获取锁,并设置锁的超时时间,保存线程标示。
  • 释放锁时先判断线程标示是否与自己的锁一致,并采用了Lua脚本保证了原子性、一致性的删除。

特性:

  • 利用setnx满足互斥
  • 利用set nx ex保证故障时锁依然能释放,避免死锁,提高安全性
  • 利用Redis集群(主从复制)保证高可用和高并发

微服务架构之:Redis的分布式锁---搭建生产可用的Redis分布式锁相关推荐

  1. 创建微服务架构的步骤_如何快速搭建一个微服务架构?

    原标题:如何快速搭建一个微服务架构? 微服务火了很久,但网上很少有文章能做到成熟地将技术传播出来,同时完美地照顾"初入微服务领域人员",从 0 开始,采用通俗易懂的语言去讲解微服务 ...

  2. 5分钟看懂微服务架构下的Consul 特性及搭建

    一.前言 虽然说牛逼的公司都有那么几个牛逼的运维团队,牛逼的运维团队都有着神秘黑科技般敲代码的姿势:本人虽然不是一个运维工程师,但是自己比较爱倒腾这些东西,会那么一点点运维姿势,虽然不算专业,但是还是 ...

  3. 2017微服务 mysql集群_微服务架构系列之Nacos集群搭建

    上回我们说到了<微服务架构系列之Nacos 配置核心概念>,这次我们讲讲Nacos 集群环境搭建. 集群模式跟我们平时进行扩容是一样的,可以通过 Nginx 转发到多个节点,如下图: 如果 ...

  4. 上几个WebAPI就算微服务架构?Too Young!

    毋庸置疑,当下是微服务云原生的时代,这是最坏的时代,也是最好的时代!机遇和挑战并行,技术人之间的差距在逐渐拉到! 两极分化严重 早在2015年,微服务就已经被诸多大企业认可和推行,被称之为微服务架构元 ...

  5. 【关于分布式系统开发和微服务架构自我心得小记

    一.一个完整项目为何要分布式开发? 完整项目业务实现是相当庞大,程序员在修改一个业务程序的的代码时,可能会影响整个项目的启动和部署,项目代码一个地方修改会产生很多问题,不利于程序的开发,同时项目的启动 ...

  6. .Net Core微服务架构技术栈的那些事

    一.前言 大家一直都在谈论微服务架构,园子里面也有很多关于微服务的文章,前几天也有一些园子的朋友问我微服务架构的一些技术,我这里就整理了微服务架构的技术栈路线图,这里就分享出来和大家一起探讨学习,同时 ...

  7. 代码重新发布后docker服务会不会受影响_分享点经验 | 浅谈微服务架构

    点击蓝字关注我们 AMP 背景简介 在最原始的系统设计中,我们通常使用单体架构.单体架构把所有的业务逻辑都写在一起,没有对业务场景进行划分.在规模比较小的情况下工作情况良好,但是随着系统规模的扩大,它 ...

  8. 《SpringCloud Alibaba 微服务架构》专题(二十一)-Seat简介与安装

    文章目录 1.业务场景 2.Seata简介 3.Seata原理和设计 4.seata-server的安装与配置 Seata是Alibaba开源的一款分布式事务解决方案,致力于提供高性能和简单易用的分布 ...

  9. .Net Core微服务架构

    目录 一.前言 二.技术栈 2.1 工欲善其事,必先利其器 2.2 微服务 2.3 微服务开源框架 2.4 ORM框架 2.5 分布式跟踪系统 2.6 系统日志集成 2.7 消息队列 2.8 任务调度 ...

最新文章

  1. Android Build.VERSION.SDK_INT
  2. 听说程序猿不会撩妹,我笑了
  3. Google 希望将 Go 打造成云端应用开发的首选语言
  4. asp.net C#生成和解析二维码代码
  5. 理请求时出现未知错误.服务器返回的状态码为: 500,react-native
  6. Makefile函数使用
  7. asp.net 设置 excel alignment_教你如何用Python轻轻松松操作Excel、Word、CSV,一文就够了,赶紧码住!!!...
  8. linux adduser mysql_linux_adduser
  9. linux添加用户命令_为Linux的cp和mv命令添加进度条
  10. python的migratetodb_Python的Flask框架中使用Flask-Migrate扩展迁移数据库的教程
  11. Django项目部署(nginx1.18+uwgsi)
  12. java 泛型 `<E extends Enum<E>>`
  13. 能力风暴智能机器人编程实例与vjc4.2的相关问题
  14. 【10】Docker的安装 --Mac
  15. 数据加密存储---加密文件系统(EFS)介绍
  16. 服装管理系统总体概述
  17. FPGA 30 综合数字ADC /DAC 信号发送采集系统设计(综合项目设计)
  18. 三分钟快速了解typeScript数据类型
  19. AtCoder Regular Contest 154 题解
  20. Ceph对象存储的使用

热门文章

  1. java 流 改变编码_Java-IO流之转换流的使用和编码与解码原理
  2. 利用opencv进行图片水印消除
  3. 基于C++的Huffman赫夫曼编译码器开发 课程论文+项目源码及可执行exe文件
  4. 大数据处理常用的数据结构
  5. html让gif图片暂停,控制GIF动画暂停播放的代码
  6. 比菜鸟更进一步(1):Style文件和toolbar的使用
  7. 扫地机器人杂牌的怎么样_小户型用扫地机器人好吗?家用扫地机器人哪个牌子好?...
  8. 企企通:数字化浪潮下,企业如何利用间接采购策略,实现降本增效?
  9. ACM—TC 联合招新赛 Round2
  10. GooglePlus API的iOS调试心得