浅谈Redis分布式锁(上)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

不论面试还是实际工作中,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关键字
  • AQS

个人觉得synchronized关键字要比AQS难理解,但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互斥吗?

t1线程执行m1方法时要去读this对象锁,但是t2线程并不需要读锁,两者各管各的,没有交集(不共用一把锁)

  • 同一个类中synchronized method m1中可以调用synchronized method m2吗?

synchronized是可重入锁,可以粗浅地理解为同一个线程在已经持有该锁的情况下,可以再次获取锁,并且会在某个状态量上做+1操作(ReentrantLock也支持重入)

  • 子类同步方法synchronized method m可以调用父类的synchronized method m吗?

子类对象初始化前,会调用父类构造方法,在结构上相当于包裹了一个父类对象,用的都是this锁对象

  • 静态同步方法和非静态同步方法互斥吗?

各玩各的,不是同一把锁,谈不上互斥

Redis分布式锁的概念

谈到Redis分布式锁,总是会有这样或那样的疑问:

  • 什么是分布式
  • 什么是分布式锁
  • 为什么需要分布式锁
  • Redis如何实现分布式锁

前3个问题其实可以一起回答,至于Redis如何实现分布式锁,我们放在下一篇。

什么是分布式?这是个很复杂的概念,我也很难说准确,所以干脆画个图,大家各花入各眼吧:

分布式有个很显著的特点是,Service A和Service B极有可能并不是部署在同一个服务器上,所以它们也不共享同一片JVM内存。而上面介绍了,要想实现线程互斥,必须保证所有访问的线程使用的是同一把锁(JVM锁此时就无法保证互斥)。

对于分布式项目,有多少台服务器就有多少片JVM内存,即使每片内存中各设置一把“独一无二”的锁,从整体来看项目中的锁就不是唯一的。

此时,如何保证每一个JVM上的线程共用一把锁呢?

答案是:把锁抽取出来,让线程们在同一片内存相遇。

但锁是不能凭空存在的,本质还是要在内存中,此时可以使用Redis缓存作为锁的宿主环境,这就是Redis能构造分布式锁的原因。

Redis的锁长啥样

synchronized关键字和ReentrantLock,它们都是实实在在已经实现的锁,而且还有标志位啥的。但Redis就是一个内存...怎么作为锁呢?

有一点大家要明确,Redis之所以能用来做分布式锁,肯定不只是因为它是一片内存,否则JVM本身也占有内存,为什么无法自己实现分布式锁呢?

我个人的理解是,要想自定义一个分布式锁,必须至少满足几个条件:

  • 多进程可见(独立于多节点系统之外的一片内存)
  • 互斥(可以通过单线程,或者某种顺序机制)
  • 可重入

还有个条件,默认要支持:只有持有这把锁的客户端才能解锁

以上三点Redis都能满足。在上面三个条件下,其实怎么设计锁,完全取决于个人如何定义锁。就好比现实生活中,通常我们理解的锁就是有个钥匙孔、需要插入钥匙的金属小物件。然而锁的形态可不止这么一种,随着科技的发展,什么指纹锁、虹膜锁层出不穷,但归根结底它们之所以被称为“锁”,是因为都保证了“互斥”(我行,你不行)。

如果我们能设计一种逻辑,它能造成某个场景下的“互斥事件”,那么它就可以被称为“锁”。比如,某家很有名的网红店,一天只接待一位客人。门口没有营业员,就放了一台取号机,里面放了一张票。你如果去迟了,票就没了,你就进不了这家店。这个场景下,没票的顾客进不去,被锁在门外。此时,取票机造成了“互斥事件”,那么它就可以叫做“锁”。

而Redis提供了setnx指令,如果某个key当前不存在则设置成功并返回true,否则不再重复设置,直接返回false。这不就是编程界的取号机吗?当然,实际用到的命令可不止这一个,具体如何实现,请看下一篇~

这一篇从JVM锁聊到了Redis分布式锁,还介绍了Java的对象内存结构及synchronized底层的原理,相信大家对“锁”已经有了自己的感性认识。下一篇我们将通过分布式定时任务的案例介绍Redis分布式锁的使用场景。

下次见。

思考一个问题:分布式系统是否一定要分布式锁?

分布式系统如果要加锁是否一定要使用分布式锁呢?

可能未必。

如果你需要的是写锁,那么可能确实需要分布式锁保证单一线程处理数据,而如果是为了防止缓存击穿(热点数据定时失效),那么使用JVM本地锁也没有太大关系。比如某个服务有10个节点,在使用JVM锁的情况下,即使某一时刻每个节点各自涌入1000个请求,虽然总共有1w个请求,但最终打到数据库的也只有10个,数据库层面是完全可以抗住这点请求量的,又由于本身是查询,所以不会造成线程安全问题。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/240485.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信息安全等级保护的定义与意义

目录 前言 信息安全等级保护定义 广义上 狭义上 技术和管理 信息安全的基本要素 信息安全等级保护的意义 当前形式 形式严峻 国家安全 三个基本一个根本 预期目标 最终效果 实际意义 前言 信息安全等级保护是对信息和信息载体按照重要性等级分级进行保护的一种…

Windows平台开发需要掌握的基础知识

windows本身也是一个软件。在这个软件中进行开发时,我们需要对它有个基础的了解,这样能让我们的开发过程更顺畅一些。 下面我就来说一下我们需要关注的基础知识点。 环境变量 有时候我们的程序执行,需要基于一些基础的库。比如Java运行&am…

matlab 最小二乘拟合平面(直接求解法)

目录 一、算法原理二、代码实现三、算法效果本文由CSDN点云侠原创,原文链接。爬虫网站自重。 一、算法原理 平面方程的一般表达式为: A x + B y +

【Skynet 入门实战练习】事件模块 | 批处理模块 | GM 指令 | 模糊搜索

文章目录 前言事件模块批处理模块GM 指令模块模糊搜索最后 前言 本节完善了项目,实现了事件、批处理、模糊搜索模块、GM 指令模块。 事件模块 什么是事件模块?事件模块是用来在各系统之间传递事件消息的。 为什么需要事件模块?主要目的是…

Spring源码分析 @Autowired 是怎样完成注入的?究竟是byType还是byName亦两者皆有

1. 五种不同场景下 Autowired 的使用 第一种情况 上下文中只有一个同类型的bean 配置类 package org.example.bean;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;Configuration public class FruitCo…

推箱子小游戏

--print("开发流程步骤:I、绘制推箱子地图并初始化 ----- 几*几大小的地图 \n\n II、根据宏定义和推箱子地图上的数字来选择不同的图形\n\n III、获取玩家坐标 -----------重点\n\n …

html旋转相册

一、实验题目 做一个旋转的3d相册 二、实验代码 <!DOCTYPE html> <html lang"zh"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport&qu…

AtomHub 开源容器镜像中心开放公测,国内服务稳定下载

由开放原子开源基金会主导&#xff0c;华为、浪潮、DaoCloud、谐云、青云、飓风引擎以及 OpenSDV 开源联盟、openEuler 社区、OpenCloudOS 社区等成员单位共同发起建设的 AtomHub 可信镜像中心正式开放公测。AtomHub 秉承共建、共治、共享的理念&#xff0c;旨在为开源组织和开…

医保购药小程序:智能合约引领医疗数字革新

在医疗领域&#xff0c;医保购药小程序通过引入智能合约技术&#xff0c;为用户提供更为高效、安全的购药体验。本文将通过简单的智能合约代码示例&#xff0c;深入探讨医保购药小程序如何利用区块链技术中的智能合约&#xff0c;实现医保结算、购药监控等功能&#xff0c;为医…

AI大模型:未来科技的新篇章

目录 1AI大模型&#xff1a;未来科技的新篇章 2AI超越数学家攻克经典数学难题&#xff1b;非侵入式设备解码大脑思维 1AI大模型&#xff1a;未来科技的新篇章 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已经成为了我们生活中不可或缺的一部分。而AI大…

Windows系统找不到xinput1_3.dll怎么办?

引言&#xff1a; 在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是xinput1_3.dll丢失。那么&#xff0c;xinput1_3.dll究竟是什么&#xff1f;为什么会出现丢失的情况&#xff1f;丢失后会对计算机产生什么影响&#xff1f;本文将详细介绍…

2023.12.21 关于 Redis 常用数据结构 和 单线程模型

目录 各数据结构具体编码方式 查看 key 对应 value 的编码方式 Reids 单线程模型 经典面试题 IO 多路复用 Redis 常用数据结构 Redis 中所有的 key 均为 String 类型&#xff0c;而不同的是 value 的数据类型却有很多种以下介绍 5 种 value 常见的数据类型 注意&#xff1…

计算机网络概述(下)——“计算机网络”

各位CSDN的uu们你们好呀&#xff0c;今天继续计算机网络概述的学习&#xff0c;下面&#xff0c;让我们一起进入计算机网络概述的世界吧&#xff01;&#xff01;&#xff01; 计算机网络体系结构 数据传输流程 计算机网络性能指标 计算机网络体系结构 两个计算机系统必须高度…

7.4组合总和(LC39-M)

算法: 组合问题&#xff0c;用回溯。 画树 回溯三部曲&#xff1a; 1.确定函数返回值和参数&#xff1a; 返回值&#xff1a;void 参数&#xff1a; candidates, target&#xff08;题目中给出的&#xff09; sum&#xff1a;统计每个组合的和&#xff0c;是否target …

鞋服用户运营策略如何实现有效闭环?

实现长期价值和业务闭环是企业经营的关键。对于鞋服行业来说&#xff0c;如何基于客户旅程编排&#xff08;Customer Journey Orchestration&#xff0c;简称 CJO&#xff09;实现用户运营策略的有效闭环&#xff0c;提升长期价值呢&#xff1f; 本文围绕该主题&#xff0c;从鞋…

C语言——小细节和小知识6

一、转义字符相关 \ 反斜杠&#xff0c;转义字符中的转义序列符 \? 将?转义&#xff0c;防止他被识别成三字母词(很早的东西)中的问号 //三字母词 //??(是[ //??)是] printf("%s","??(??)"); //打印结果是[] 二、fopen函数fc…

Vue2+Vue3组件间通信方式汇总(2)------$emit

组件间通信方式是前端必不可少的知识点&#xff0c;前端开发经常会遇到组件间通信的情况&#xff0c;而且也是前端开发面试常问的知识点之一。接下来开始组件间通信方式第二弹------$emit,并讲讲分别在Vue2、Vue3中的表现。 Vue2Vue3组件间通信方式汇总&#xff08;1&#xff0…

【C++】STL 容器 - stack 堆栈容器 ① ( stack 堆栈容器特点 | stack 堆栈容器与 deque 双端数组容器对比 | 简单示例 )

文章目录 一、 stack 堆栈容器简介1、stack 堆栈容器引入2、stack 堆栈容器特点3、stack 堆栈容器与 deque 双端数组容器对比 二、 代码示例 - stack 堆栈容器简单示例1、代码示例2、执行结果 一、 stack 堆栈容器简介 1、stack 堆栈容器引入 C 语言中的 STL 标准模板库 中的 s…

ABS210-ASEMI手机适配器整流桥ABS210

编辑&#xff1a;ll ABS210-ASEMI手机适配器整流桥ABS210 型号&#xff1a;ABS210 品牌&#xff1a;ASEMI 封装&#xff1a;ABS-4 特性&#xff1a;贴片、整流桥 最大平均正向电流&#xff1a;2A 最大重复峰值反向电压&#xff1a;1000V 恢复时间&#xff1a;&#xff…

步兵 cocos2dx 加密和混淆

文章目录 摘要引言正文代码加密具体步骤代码加密具体步骤测试和配置阶段IPA 重签名操作步骤 总结参考资料 摘要 本篇博客介绍了针对 iOS 应用中的 Lua 代码进行加密和混淆的相关技术。通过对 Lua 代码进行加密处理&#xff0c;可以确保应用代码的安全性&#xff0c;同时提高性…