多个项目共用同一个redis_浅谈Redis分布式锁(上)
不论面试还是实际工作中,Redis都是避无可避的技术点。在我心里,MySQL和Redis是衡量一个程序员是否“小有所成”的两把标尺。如果他能熟练使用MySQL和Redis,以小化大,充分利用现有资源出色地完成当下需求,说明他已经成长了。
本篇文章我们一起来探讨Redis分布式锁相关的内容。
说到锁,大家第一时间想到的应该是synchronized关键字或ReentrantLock,随即想到偏向锁、自旋锁、重量级锁或者CAS甚至AQS。一般来说,我不喜欢一下子引入这么多概念,可能会把问题弄复杂,但为了方便大家理解Redis分布式锁,这里稍微提一下。
JVM锁
所谓JVM锁,其实指的是诸如synchronized关键字或者ReentrantLock实现的锁。之所以统称为JVM锁,是因为我们的项目其实都是跑在JVM上的。理论上每一个项目启动后,就对应一片JVM内存,后续运行期时数据的生离死别都是在这一片土地上。
什么是锁、怎么锁?
明白了“JVM锁”名字的由来,我们再来聊什么是“锁”,以及怎么“锁”。
有时候我们很难阐述清楚某个事物是什么,但很容易解释它能干什么,JVM锁也是这个道理。JVM锁的出现,就是为了解决线程安全问题。所谓线程安全问题,可以简单地理解为数据不一致(与预期不一致)。
什么时候可能出现线程安全问题呢?
当同时满足以下三个条件时,才可能引发线程安全问题:
- 多线程环境
- 有共享数据
- 有多条语句操作共享数据/单条语句本身非原子操作(比如i++虽然是单条语句,但并非原子操作)
比如线程A、B同时对int count进行+1操作(初始值假设为1),在一定的概率下两次操作最终结果可能为2,而不是3。
那么加锁为什么能解决这个问题呢?
如果不考虑原子性、内存屏障等晦涩的名词,加锁之所以能保证线程安全,核心就是“互斥”。所谓互斥,就是字面意思上的互相排斥。这里的“互相”是指谁呢?就是多线程之间!
怎么实现多线程之间的互斥呢?
引入“中间人”即可。
注意,这是个非常简单且伟大的思想。在编程世界中,通过引入“中介”最终解决问题的案例不胜枚举,包括但不限于Spring、MQ。在码农之间,甚至流传着一句话:没有什么问题是引入中间层解决不了的。
而JVM锁其实就是线程和线程彼此的“中间人”,多个线程在操作加锁数据前都必须去问问“中间人”它是否允许当前线程操作这个数据:
锁在这里扮演的角色其实就是守门员,是唯一的访问入口,所有的线程都要经过它的拷问。在JDK中,锁的实现机制最常见的就是两种,分别是两个派系:
- synchronized关键字
- CAS+AQS
个人觉得synchronized关键字要比CAS+AQS难理解,但CAS+AQS的源码比较抽象。这里简要介绍一下Java对象内存结构和synchronized关键字的实现原理。
Java对象内存结构
要了解synchronized关键字,首先要知道Java对象的内存结构。强调一遍,是Java对象的内存结构。
它的存在仿佛向我们抛出一个疑问:如果有机会解剖一个Java对象,我们能看到什么?
右上图画了两个对象,只看其中一个即可。我们可以观察到,Java对象内存结构大致分为几块:
- Mark Word(锁相关)
- 元数据指针(class pointer,指向当前实例所属的类)
- 实例数据(instance data,我们平常看到的仅仅是这一块)
- 对齐(padding,和内存对齐有关)
如果此前没有了解过Java对象的内存结构,你可能会感到吃惊:天呐,我还以为Java对象就只有属性和方法!
是的,我们最熟悉实例数据这一块,而且以为只有这一块。也正是这个观念的限制,导致一部分初学者很难理解synchronized。比如初学者经常会疑惑:
- 为什么任何对象都可以作为锁?
- Object对象锁和类锁有什么区别?
- synchronized修饰的普通方法使用的锁是什么?
- synchronized修饰的静态方法使用的锁是什么?
这一切的一切,其实都可以在Java对象内存结构中的Mark Word找到答案:
很多同学可能是第一次看到这幅图,会感到有点懵,没关系,我也很头大,都一样的。
Mark Word包含的信息还是蛮多的,但这里我们只需要简单地把它理解为记录锁信息的标记即可。上图展示的是32位虚拟机下的Java对象内存,如果你仔细数一数,会发现全部bit加起来刚好是32位。64位虚拟机下的结构大同小异,就不特别介绍。
Mark Word从有限的32bit中划分出2bit,专门用作锁标志位,通俗地讲就是标记当前锁的状态。
正因为每个Java对象都有Mark Word,而Mark Word能标记锁状态(把自己当做锁),所以Java中任意对象都可以作为synchronized的锁:
synchronized(person){}
synchronized(student){}
所谓的this锁就是当前对象,而Class锁就是当前对象所属类的Class对象,本质也是Java对象。synchronized修饰的普通方法底层使用当前对象作为锁,synchronized修饰的静态方法底层使用Class对象作为锁。
但如果要保证多个线程互斥,最基本的条件是它们使用同一把锁:
对同一份数据加两把不同的锁是没有意义的,实际开发时应该注意避免下面的写法:
synchronized(Person.class){// 操作count
}synchronized(person){// 操作count
}
或者
public synchronized void method1(){// 操作count
}public static synchronized void method1(){// 操作count
}
synchronized与锁升级
大致介绍完Java对象内存结构后,我们再来解决一个新疑问:
为什么需要标记锁的状态呢?是否意味着synchronized锁有多种状态呢?
在JDK早期版本中,synchronized关键字的实现是直接基于重量级锁的。只要我们在代码中使用了synchronized,JVM就会向操作系统申请锁资源(不论当前是否真的是多线程环境),而向操作系统申请锁是比较耗费资源的,其中涉及到用户态和内核态的切换等,总之就是比较费事,且性能不高。
JDK为了解决JVM锁性能低下的问题,引入了ReentrantLock,它基于CAS+AQS,类似自旋锁。自旋的意思就是,在发生锁竞争的时候,未争取到锁的线程会在门外采取自旋的方式等待锁的释放,谁抢到谁执行。
自旋锁的好处是,不需要兴师动众地切换到内核态申请操作系统的重量级锁,在JVM层面即可实现自旋等待。但世界上并没有百利而无一害的灵丹妙药,CAS自旋虽然避免了状态切换等复杂操作,却要耗费部分CPU资源,尤其当可预计上锁的时间较长且并发较高的情况下,会造成几百上千个线程同时自旋,极大增加CPU的负担。
synchronized毕竟JDK亲儿子,所以大概在JDK1.6或者更早期的版本,官方对synchronized做了优化,提出了“锁升级”的概念,把synchronized的锁划分为多个状态,也就是上图中提到的:
- 无锁
- 偏向锁
- 轻量级锁(自旋锁)
- 重量级锁
无锁就是一个Java对象刚new出来的状态。当这个对象第一次被一个线程访问时,该线程会把自己的线程id“贴到”它的头上(Mark Word中部分位数被修改),表示“你是我的”:
此时是不存在锁竞争的,所以并不会有什么阻塞或等待。
为什么要设计“偏向锁”这个状态呢?
大家回忆一下,项目中并发的场景真的这么多吗?并没有吧。大部分项目的大部分时候,某个变量都是单个线程在执行,此时直接向操作系统申请重量级锁显然没有必要,因为根本不会发生线程安全问题。
而一旦发生锁竞争时,synchronized便会在一定条件下升级为轻量级锁,可以理解为一种自旋锁,具体自旋多少次以及何时放弃自旋,JDK也有一套相关的控制机制,大家可以自行了解。
同样是自旋,所以synchronized也会遇到ReentrantLock的问题:如果上锁时间长且自旋线程多,又该如何?
此时就会再次升级,变成传统意义上的重量级锁,本质上操作系统会维护一个队列,用空间换时间,避免多个线程同时自旋等待耗费CPU性能,等到上一个线程结束时唤醒等待的线程参与新一轮的锁竞争即可。
synchronized案例
让我们一起来看几个案例,加深对synchronized的理解。
- 同一个类中的synchronized method m1和method m2互斥吗?
- 同一个类中synchronized method m1中可以调用synchronized method m2吗?
- 子类同步方法synchronized method m可以调用父类的synchronized method m吗?
- 静态同步方法和非静态同步方法互斥吗?
Redis分布式锁
谈到Redis分布式锁,总是会有这样或那样的疑问:
- 什么是分布式
- 什么是分布式锁
- 为什么需要分布式锁
- Redis如何实现分布式锁
前3个问题其实可以一起回答,至于Redis如何实现分布式锁,我们放在下一篇。
什么是分布式?这是个很复杂的概念,我也很难说准确,所以干脆画个图,大家各花入各眼吧:
分布式有个很显著的特点是,Service A和Service B极有可能并不是部署在同一个服务器上,所以它们也不共享同一片JVM内存。而上面介绍了,要想实现线程互斥,必须保证所有访问的线程使用的是同一把锁(JVM锁此时就无法保证互斥)。
对于分布式项目,有多少台服务器就有多少片JVM内存,即使每片内存中各设置一把“独一无二”的锁,从整体来看项目中的锁就不是唯一的。
此时,如何保证每一个JVM上的线程共用一把锁呢?
答案是:把锁抽取出来,让线程们在同一片内存相遇。
但锁是不能凭空存在的,本质还是要在内存中,此时可以使用Redis缓存作为锁的宿主环境,这就是Redis能构造分布式锁的原因。
这一篇从JVM锁聊到了Redis分布式锁,还介绍了Java的对象内存结构及synchronized底层的原理,相信大家对“锁”已经有了自己的感性认识。下一篇我们将通过分布式定时任务的案例介绍Redis分布式锁的使用场景。
下次见。
本文来自Java小册,欢迎大家加入我们一起学习。本周末(2020-11-07)群里还有面试分享。
bravo1988:中级Java程序员如何进阶zhuanlan.zhihu.com
多个项目共用同一个redis_浅谈Redis分布式锁(上)相关推荐
- 浅谈Redis分布式锁的进化史
近两年来微服务变得越来越热门,越来越多的应用部署在分布式环境中,在分布式环境中,数据一致性是一直以来需要关注并且去解决的问题,分布式锁也就成为了一种广泛使用的技术,常用的分布式实现方式为Redis,Z ...
- 多个项目共用同一个redis_分区:如何在多个Redis实例之间拆分数据
分区是将数据拆分为多个Redis实例的过程,因此每个实例将仅包含键的一个子集. 本文档的第一部分将向您介绍分区的概念,第二部分将向您展示Redis分区的替代方法. 为什么分区是有用的 对Redis进行 ...
- 五分钟DBA:浅谈伪分布式数据库架构
[IT168 技术]12月25日消息,2010互联网行业技术研讨峰会今日在上海华东理工大学召开.本次峰会以"互联网行业应用最佳实践"为主题,定位于互联网架构设计.应用开发.应用运维 ...
- 浅谈线程池(上):线程池的作用及CLR线程池
线程池是一个重要的概念.不过我发现,关于这个话题的讨论似乎还缺少了点什么.作为资料的补充,以及今后文章所需要的引用,我在这里再完整而又简单地谈一下有关线程池,还有.NET中各种线程池的基础.更详细的内 ...
- Python 基于python+mysql浅谈redis缓存设计与数据库关联数据处理
基于python+mysql浅谈redis缓存设计与数据库关联数据处理 by:授客 QQ:1033553122 测试环境 redis-3.0.7 CentOS 6.5-x86_64 python 3 ...
- 并发执行变成串行_大神浅谈数据库并发控制 锁和 MVCC
在学习几年编程之后,你会发现所有的问题都没有简单.快捷的解决方案,很多问题都需要权衡和妥协,而本文介绍的就是数据库在并发性能和可串行化之间做的权衡和妥协 - 并发控制机制.  如果数据库中的所有事务 ...
- 浅谈数据库设计技巧(上)
浅谈数据库设计技巧(上) 说到数据库,我认为不能不先谈数据结构.1996年,在我初入大学学习计算机编程时,当时的老师就告诉我们说:计算机程序=数据结构+算法.尽管现在的程序开发已由面向过程为主逐步过渡 ...
- python文本框与数据库的关联_Python 基于python+mysql浅谈redis缓存设计与数据库关联数据处理...
基于python+mysql浅谈redis缓存设计与数据库关联数据处理 by:授客 QQ:1033553122 测试环境 redis-3.0.7 CentOS 6.5-x86_64 python 3. ...
- Redis设计与实现 -- 浅谈Redis持久化
在讲解Redis持久化相关的话题之前,我们需要了解的是Redis为什么这么快?也就是Redis的IO模型 – 多路复用. 我们一句话概括为什么Redis这么快: Redis是单线程的,使用多路复用的I ...
- 浅谈数据库并发控制 - 锁和 MVCC
文章写得不错,原文地址见 http://draveness.me/database-concurrency-control.html 在学习几年编程之后,你会发现所有的问题都没有简单.快捷的解决方案, ...
最新文章
- 51nod 1381 硬币游戏 概率
- 机器学习实战:训练自己的YoloV5 [草稿-待完成]
- 【Android 修炼手册】常用技术篇 -- Android 热修复解析
- linux词语大全,简单词语大全二字学习软件-简单词语大全四字下载v1.5.3-Linux公社...
- 搜狗开源srpc:自研高性能通用RPC框架
- 提示microsoft incremental linker已停止工作解决方法
- new关键字执行过程
- 如何从右键弹出菜单中清空删除数据加清空前提问确定
- 传智播客 Web静态服务器-6-epoll
- EditPlus Version 3 价格 代理商 销售价格 正版软件价格
- Unity 不使用BMFont创建Font字体
- python游戏程序代码大全_童年游戏,Python一行代码就能实现!
- Error: new BigNumber() not a base 16 number
- 一位百度AI工程师的求职经历(offer/面经/干货/感悟)
- gtx1050ti安装win10和ubuntu16.04双系统
- Had I not seen the Sun(如果我不曾见过太阳)
- iwifi 技术规范
- 怎么在html的表格中加筛选,excel中表头合并单元格的筛选
- 在EXCEL表格中经常会遇到有合并单元格时,汇总计算的公式无法直接下拉自动填充计算,掌握这个小技巧一键汇总
- Vue--解决官网网站404的问题