沉梦手记 沉梦手记
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 数据库基础
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • 网络通信
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 运维与部署
  • 音视频处理
  • 随笔
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
Github (opens new window)

沉梦听雨

时间是最好的浸渍剂,而沉淀是最好的提纯器🚀
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 数据库基础
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • 网络通信
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 运维与部署
  • 音视频处理
  • 随笔
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
Github (opens new window)
  • 脚手架搭建
  • 瑞吉外卖

  • 黑马点评

    • Redis实战笔记
    • 登录与缓存优化
    • 优惠券秒杀
    • 分布式锁
      • 4. 你是怎么解决集群环境下的并发问题?
        • 1. 分布式锁的实现
        • 2. Redis 分布式锁误删情况
        • 3. 分布式锁的原子性问题
        • 4. 基于 SETNX 实现分布式锁存在的问题
        • Redisson 可重入锁原理
        • Redisson 锁的 MultiLock 原理
        • 小结
    • 秒杀优化与消息队列
    • 社交功能与Feed流
    • GEO与签到统计
    • 面试问答整理
    • 电商场景分析
    • 项目技术亮点
  • vue-blog

  • 沉梦接口开放平台

  • 用户中心

  • 聚合搜索平台

  • 仿12306项目

  • 壁纸小程序项目

  • RuoYi-Vue

  • 项目笔记
  • 黑马点评
沉梦听雨
2023-06-08
目录

分布式锁

# 分布式锁

# 4. 你是怎么解决集群环境下的并发问题?

原因分析:由于我们部署了多个 Tomcat,每个 Tomcat 都有一个属于自己的 JVM。那么假设在服务器 A 的 Tomcat 内部,有两个线程,即线程 1 和线程 2,这两个线程使用的是同一份代码,那么它们的锁对象是同一个,是可以实现互斥的。但是如果在服务器 B 的 Tomcat 内部,又有两个线程,但是它们的锁对象虽然写的和服务器 A 一样,但是锁对象却不是同一个,所以线程 3 和线程 4 可以实现互斥,但是却无法和线程 1 和线程 2 互斥。

image.png

这就是集群环境下 syn 锁失效的原因。在这种情况下,我们需要使用分布式锁来解决这个问题,让锁不存在于每个 JVM 的内部,而是让所有 JVM 公用外部的一把锁(Redis)。

# 1. 分布式锁的实现

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

分布式锁的核心思想就是让大家共用同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路。

image.png

分布式锁应该满足的条件:

  1. 可见性:多个线程都能看到相同的结果。注意:这里说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思。
  2. 互斥:互斥是分布式锁的最基本条件,使得程序串行执行。
  3. 高可用:程序不易崩溃,时时刻刻都保证较高的可用性。
  4. 高性能:由于加锁本身就让性能降低,所以对于分布式锁需要较高的加锁性能和释放锁性能。
  5. 安全性:安全也是程序中必不可少的一环。

常见的分布式锁有三种:

  1. MySQL:MySQL 本身就带有锁机制,但是由于 MySQL 的性能一般,所以采用分布式锁的情况下,使用 MySQL 作为分布式锁比较少见。
  2. Redis:Redis 作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都是用 Redis 或者 Zookeeper 作为分布式锁。利用 SETNX 这个方法,如果插入 Key 成功,则表示获得到了锁;如果有人插入成功,那么其他人就会插入失败,无法获取到锁。利用这套逻辑完成互斥,从而实现分布式锁。
  3. Zookeeper:Zookeeper 也是企业级开发中较好的一种实现分布式锁的方案,但本文是学 Redis 的,所以这里就不过多阐述了。
MySQL Redis Zookeeper
互斥 利用 MySQL 本身的互斥锁机制 利用 setnx 这样的互斥命令 利用节点的唯一性和有序性实现互斥
高可用 好 好 好
高性能 一般 好 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放

我这里使用的 Redis 的 setnx 命令。

获取锁:

  • 互斥:确保只能有一个线程获取锁
  • 非阻塞:尝试一次,成功返回 true,失败返回 false
SET lock thread01 NX EX 10
1

释放锁:

  • 手动释放
  • 超时释放:获取锁的时候添加一个超时时间
DEL lock
1

核心思路:我们利用 Redis 的 SETNX 方法,当有多个线程进入时,我们就利用该方法来获取锁。第一个线程进入时,Redis 中就有这个 key 了,返回了 1,如果结果是 1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑。没有抢到锁(返回了 0)的线程,等待一定时间之后重试。

# 2. Redis 分布式锁误删情况

逻辑说明:

  • 持有锁的线程 1 在锁的内部出现了阻塞,导致他的锁 TTL 到期,自动释放。
  • 此时线程 2 也来尝试获取锁,由于线程 1 已经释放了锁,所以线程 2 可以拿到。
  • 但是现在线程 1 阻塞完了,继续往下执行,要开始释放锁了。
  • 那么此时就会将属于线程 2 的锁释放,这就是误删别人锁的情况。

解决方案:

  • 解决方案就是在每个线程释放锁的时候,都判断一下这个锁是不是自己的,如果不属于自己,则不进行删除操作。
  • 假设还是上面的情况,线程 1 阻塞,锁自动释放,线程 2 进入到锁的内部执行逻辑。此时线程 1 阻塞完了,继续往下执行,开始删除锁,但是线程 1 发现这把锁不是自己的,所以不进行删除锁的逻辑。当线程 2 执行到删除锁的逻辑时,如果 TTL 还未到期,则判断当前这把锁是自己的,于是删除这把锁。

image.png

解决 Redis 分布式锁误删问题:

  • 需求:修改之前的分布式锁实现。
  • 满足:在获取锁的时候存入线程标识(用 UUID 标识,在一个 JVM 中,ThreadId 一般不会重复,但是我们现在是集群模式,有多个 JVM,多个 JVM 之间可能会出现 ThreadId 重复的情况),在释放锁的时候先获取锁的线程标识,判断是否与当前线程标识一致。
    • 如果一致则释放锁
    • 如果不一致则不释放锁
  • 核心逻辑:在存入锁的时候,放入自己的线程标识,在删除锁的时候,判断当前这把锁是不是自己存入的。
    • 如果是,则进行删除
    • 如果不是,则不进行删除

# 3. 分布式锁的原子性问题

更为极端的误删逻辑说明:

  • 假设线程 1 已经获取了锁,在判断标识一致之后,准备释放锁的时候,又出现了阻塞(例如 JVM 垃圾回收机制)。
  • 于是锁的 TTL 到期了,自动释放了。
  • 那么现在线程 2 趁虚而入,拿到了一把锁。
  • 但是线程 1 的逻辑还没执行完,那么线程 1 就会执行删除锁的逻辑。
  • 但是在阻塞前线程 1 已经判断了标识一致,所以现在线程 1 把线程 2 的锁给删了。
  • 那么就相当于判断标识那行代码没有起到作用。
  • 这就是删锁时的原子性问题。
  • 因为线程 1 的拿锁、判断标识、删锁不是原子操作,所以我们要防止刚刚的情况。

image.png

解决方案:使用 Redis 提供的 Lua 脚本实现原子性

  • Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性。
  • Lua 是一种编程语言,它的基本语法可以上菜鸟教程看看,链接:https://www.runoob.com/lua/lua-tutorial.html (opens new window)
  • 这里重点介绍 Redis 提供的调用函数,我们可以使用 Lua 去操作 Redis,而且还能保证它的原子性,这样就可以实现拿锁、判断标识、删锁是一个原子性动作了。

原逻辑:

@Override
public void unlock() {
    // 获取当前线程的标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标识
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标识是否一致
    if (threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

改为 Lua 脚本:

-- 这里的KEYS[1]就是传入锁的key
-- 这里的ARGV[1]就是线程标识
-- 比较锁中的线程标识与线程标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
  -- 一致则释放锁
  return redis.call('del', KEYS[1])
end
return 0
1
2
3
4
5
6
7
8

利用 Java 代码调用 Lua 脚本改造分布式锁:

在 RedisTemplate 中,可以利用 execute 方法去执行 Lua 脚本:

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
    return this.scriptExecutor.execute(script, keys, args);
}
1
2
3

对应的 Java 代码如下:

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static {
    UNLOCK_SCRIPT = new DefaultRedisScript();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    UNLOCK_SCRIPT.setResultType(Long.class);
}

@Override
public void unlock() {
    stringRedisTemplate.execute(UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4. 基于 SETNX 实现分布式锁存在的问题

具体笔记:https://cyborg2077.github.io/2022/10/22/RedisPractice/#%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81-Redisson (opens new window)

具体视频:https://www.bilibili.com/video/BV1cr4y1671t?p=64 (opens new window)

  1. 重入问题
    • 重入问题是指获取锁的线程,可以再次进入到相同的锁的代码块中。可重入锁的意义在于防止死锁,例如在 HashTable 这样的代码中,它的方法都是使用 synchronized 修饰的,假如它在一个方法内调用另一个方法,如果此时是不可重入的,那就死锁了。所以可重入锁的主要意义是防止死锁,我们的 synchronized 和 Lock 锁都是可重入的。
  2. 不可重试
    • 我们编写的分布式锁只能尝试一次,失败了就返回 false,没有重试机制。但合理的情况应该是:当线程获取锁失败后,他应该能再次尝试获取锁。
  3. 超时释放
    • 我们在加锁的时候增加了 TTL,这样我们可以防止死锁,但是如果卡顿(阻塞)时间太长,也会导致锁的释放。虽然我们采用 Lua 脚本来防止删锁的时候误删别人的锁,但现在的新问题是没锁住,也有安全隐患。
  4. 主从一致性
    • 如果 Redis 提供了主从集群,那么当我们向集群写数据时,主机需要异步地将数据同步给从机。万一在同步之前,主机宕机了(主从同步存在延迟,虽然时间很短,但还是发生了),那么又会出现死锁问题。

那么什么是 Redisson 呢?

  • Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式 Java 常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redis 提供了分布式锁的多种多样功能:

  1. 可重入锁(Reentrant Lock)
  2. 公平锁(Fair Lock)
  3. 联锁(MultiLock)
  4. 红锁(RedLock)
  5. 读写锁(ReadWriteLock)
  6. 信号量(Semaphore)
  7. 可过期性信号量(PermitExpirableSemaphore)
  8. 闭锁(CountDownLatch)

# Redisson 可重入锁原理

  • 在 Lock 锁中,它是借助于底层的一个 volatile 的 state 变量来记录重入状态的:
    • 如果当前没有人持有这把锁,那么 state = 0
    • 如果有人持有这把锁,那么 state = 1
      • 如果持有这把锁的人再次持有这把锁,那么 state 会 +1
    • 如果对于 synchronize 而言,它在 C 语言代码中会有一个 count
    • 原理与 state 类似,也是重入一次就 +1,释放一次就 -1,直至减到 0,表示这把锁没有被人持有
  • 在 Redisson 中,我们也支持可重入锁
    • 在分布式锁中,它采用 hash 结构来存储锁,其中外层 key 表示这把锁是否存在,内层 key 则记录当前这把锁被哪个线程持有
  • method1 在方法内部调用 method2,method1 和 method2 处于同一个线程,那么 method1 已经拿到一把锁了,想进入 method2 中拿另外一把锁,必然是拿不到的,于是就出现了死锁

image.png

WatchDog 看门狗功能:Redisson 的 WatchDog 是一个用于监听分布式场景下数据变化的组件。它会监控一个或多个 Redis 键的变化,并在发生变化时触发回调函数。WatchDog 可以用来处理一些场景,比如数据变化后需要进行特定的业务处理。WatchDog 可以监控多个 Redis 键,支持多个回调函数,支持异步回调和同步回调,支持对监控频率的控制,可以根据不同的情况下调整监控频率,以达到最优化的性能。同时,加入了 Redisson 的分布式锁功能,能够有效处理分布式场景下的并发更新问题。

# Redisson 锁的 MultiLock 原理

  • 为了提高 Redis 的可用性,我们会搭建集群或者主从,现在以主从为例。
  • 此时我们去写命令,写在主机上,主机会将数据同步给从机,但是假设主机还没来得及把数据写入到从机去的时候,主机宕机了。
  • 哨兵会发现主机宕机了,于是选举一个 slave(从机)变成 master(主机),而此时新的 master(主机)上并没有锁的信息,那么其他线程就可以获取锁,又会引发安全问题。
  • 为了解决这个问题,Redisson 提出来了 MultiLock 锁。使用这把锁的话,那我们就不用主从了,每个节点的地位都是一样的,都可以当做是主机。那我们就需要将加锁的逻辑写入到每一个主从节点上,只有所有的服务器都写入成功,此时才是加锁成功。假设现在某个节点挂了,那么他去获取锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

# 小结

  1. 不可重入 Redis 分布式锁
    • 原理:利用 SETNX 的互斥性;利用 EX 避免死锁;释放锁时判断线程标识
    • 缺陷:不可重入、无法重试、锁超时失效
  2. 可重入 Redis 分布式锁
    • 原理:利用 Hash 结构,记录线程标识与重入次数;利用 WatchDog 延续锁时间;利用信号量控制锁重试等待
    • 缺陷:Redis 宕机引起锁失效问题
  3. Redisson 的 MultiLock
    • 原理:多个独立的 Redis 节点,必须在所有节点都获取重入锁,才算获取锁成功
上次更新: 2026/10/10 18:05:45
优惠券秒杀
秒杀优化与消息队列

← 优惠券秒杀 秒杀优化与消息队列→

Theme by Vdoing | Copyright © 2023-2026 沉梦听雨 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式