Best practice-生产环境中加锁的最佳实践

请添加图片描述

什么是死锁?

场景:图书馆有两个相邻的储物柜(柜子A和柜子B),小明和小红需要同时使用这两个柜子才能完成借书流程。

  1. 互斥资源
    每个柜子只有一把钥匙,且一次只能被一人使用(资源不可共享)。
  2. 持有并等待
    • 小明拿到了柜子A的钥匙,但他说:“我要等小红用完柜子B的钥匙,才能继续操作。”
    • 小红拿到了柜子B的钥匙,但她说:“我要等小明用完柜子A的钥匙,才能继续操作。”
  1. 僵局形成
    两人都死死攥着已有的钥匙,同时等待对方手里的另一把钥匙。结果两人卡在原地,谁也无法完成借书流程。

死锁的定义:线程A获取到资源1,需要再次获取到资源2被释放以获取到该资源,此时线程B获取到了资源2,等待获取资源1,两线程进入了互相等待的状态形成死锁。

请添加图片描述

业务中的死锁

让我们以电商系统中的「购物车库存锁定」场景为例,具体分析死锁的触发机制:当用户A尝试同时锁定商品X和Y的库存,而用户B以相反顺序(先锁Y再锁X)发起操作时,这两个并发请求可能因资源竞争进入相互等待状态——此时系统既无法完成库存扣减,也无法释放已占用的资源,形成典型的死锁僵局。

MockData类初始化了商品的库存,使用ConcurrentHashp模拟购物车和商品库存信息,并提供了添加至购物车、清空购物车、扣减库存等方法。

public class MockData {// 模拟购物车:用户ID -> 商品列表 + 总价public static final ConcurrentHashMap<Long, Cart> Carts = new ConcurrentHashMap<>();// 模拟库存:商品ID -> 库存数量public static final ConcurrentHashMap<Long, AtomicInteger> Inventory = new ConcurrentHashMap<>();static {// 初始化库存(商品1和2各有1件)Inventory.put(1L, new AtomicInteger(1));Inventory.put(2L, new AtomicInteger(1));}// 添加商品到购物车public static void addToCart(Long userId, Long productId, int quantity) {Carts.computeIfAbsent(userId, k -> new Cart()).addProduct(productId, quantity);}// 清空购物车public static void clearCart(Long userId) {Carts.remove(userId);}// 获取库存数量public static int getStock(Long productId) {return Inventory.getOrDefault(productId, new AtomicInteger(0)).get();}// 扣减库存(原子操作)public static boolean decreaseStock(Long productId, int quantity) {return Inventory.getOrDefault(productId, new AtomicInteger(0)).compareAndSet(getStock(productId), getStock(productId) - quantity);}// 购物车类static class Cart {private final ConcurrentHashMap<Long, Integer> items = new ConcurrentHashMap<>();@Getterprivate int totalPrice = 0;/*** 购物车添加商品* @param productId 商品id* @param quantity 数量*/public void addProduct(Long productId, int quantity) {items.put(productId, items.getOrDefault(productId, 0) + quantity);totalPrice += quantity;}public void removeProduct(Long productId) {items.remove(productId);totalPrice -= items.getOrDefault(productId, 0);}}
}

以下代码模拟库存不足造成死锁的场景:

@Slf4j
@Service
@EnableAsync
public class OrderService {// 死锁场景(模拟库存不足)public void createOrderDeadLock(Long userId, Long productId) {log.info("用户:{},开始下单商品:{}", userId, productId);// 模拟购物车添加商品cartLock.lock();try {// 步骤2:检查库存(此时可能有其他线程扣减)if (MockData.getStock(productId) < 1) {log.error("用户:{} 库存不足,放弃订单", userId);return;}// 模拟长时间业务操作(人为制造时间差)try {Thread.sleep(5000);} catch (Exception e) {log.error("异常", e);}// 步骤3:扣减库存(实际业务场景需要原子操作)if (!MockData.decreaseStock(productId, 1)) {log.error("用户:{} 库存已被抢光,放弃订单", userId);return;}log.info("用户 {},下单成功", userId);MockData.clearCart(userId);} catch (Exception e) {log.error("下单失败", e);} finally {cartLock.unlock();}}
}

在Controller层调用该方法,同时进行场景分析:

死锁场景分析:

  • 核心逻辑:两个用户同时抢购同一商品,库存仅剩1件。
  • 死锁原因
    1. 线程1持有购物车锁,等待库存锁。
    2. 线程2持有购物车锁,等待库存锁。
    3. 双方互相等待对方释放锁,形成循环等待。
    @GetMapping("/wrong/cert/lock")public void wrongCertLock() throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(2);// 用户A尝试购买商品1(库存1)executor.submit(() -> orderService.createOrderLockBySequence(1L, 1L));// 用户B尝试购买商品1(库存已不足)executor.submit(() -> orderService.createOrderLockBySequence(2L, 1L));// 等待观察结果(死锁表现为长时间无输出)executor.shutdown();executor.awaitTermination(20, TimeUnit.SECONDS);}
}

使用工具调用该接口,并查看接口的输出结果,接口响应时间5.08秒:

请添加图片描述

在下单时使用购物车的全局锁certLock时,存在两个问题:

一.单锁阻塞堆积(隐性"假死锁")

当使用全局锁 cartLock 时,所有下单请求必须串行执行。在高并发场景下:

  1. 第一个线程获得锁后执行5秒休眠
  2. 后续所有线程在 cartLock.lock()处排队阻塞
  3. 线程堆积导致系统吞吐量骤降,最终表现类似"死锁"

数据示例:

  • 假设QPS=100,5秒内会堆积500个等待线程
  • 实际业务处理能力被压缩到0.2 TPS(每秒处理0.2个请求)

使用Arthasthread -b命令分析服务存在线程“死锁”的情况和线程阻塞情况,同时Arthas支持查看阻塞位置的源码。

请添加图片描述

使用jad --jad --source-only命令查看源码,如例子中展示第40行附近存在线程阻塞的问题,我们可以通过反编译查看源码:

jad --source-only com.codetree.business_error.chapter.chapter02.shop.OrderService createOrderDeadLock

请添加图片描述

二、锁粒度错位导致的竞态条件
隐患根源:
// 非原子操作
if (MockData.getStock(productId) < 1) {return;
}
// 非原子操作
MockData.decreaseStock(productId, 1)    

即使有全局锁保护:

  1. 库存检查与扣减分离:其他系统(如支付系统)可能同时修改库存
  2. 超卖风险:检查时库存充足,但扣减时已被其他通道(API/后台)修改

如何避免死锁?

避免死锁一般有两种方案:

方案实现方式优点缺点
一次性获取资源使用全局锁(globalLock简单粗暴,彻底避免死锁并发性能差,所有请求串行执行
按顺序获取资源固定锁顺序(先购物车 → 再库存)兼顾并发性能,适用于复杂业务需全局统一锁顺序策略
方案一、一次性获取所有资源

一次性获取所有资源可以视为将多个非原子性操作封装成一个大的原子性操作,强制实现线程“串行化”访问,该方案能够彻底消除持有并等待条件,同时保证临界区操作的原子一致性。

    public void createOrderLockAllResource(Long userId, Long productId) {log.info("优化:一次性获取所有的资源,用户:{},开始下单商品:{}", userId, productId);// 模拟购物车添加商品globalLock.lock();try {// 步骤2:检查库存(此时可能有其他线程扣减)if (MockData.getStock(productId) < 1) {log.error("用户:{} 库存不足,放弃订单", userId);return;}// 模拟长时间业务操作(人为制造时间差)try {Thread.sleep(10000);} catch (Exception e) {log.error("异常", e);}// 步骤3:扣减库存(实际业务场景需要原子操作)if (!MockData.decreaseStock(productId, 1)) {log.error("用户:{} 库存已被抢光,放弃订单", userId);return;}log.info("用户 {},下单成功", userId);MockData.clearCart(userId);} catch (Exception e) {log.error("下单失败", e);} finally {globalLock.unlock();}}

未出现阻塞问题,串行化执行成功,接口响应10.10秒。

请添加图片描述

方案二、按顺序获取资源

顺序化获取资源可以有效规避死锁产生的必要条件(之一)——循环等待条件,同时消除进程间非原子操作的竞争冲突,从而避免竞态条件的发生。

    public void createOrderLockBySequence(Long userId, Long productId) {log.info("优化:按顺序获取锁,用户:{},开始下单商品:{}", userId, productId);// 模拟购物车添加商品cartLock.lock();try {// 步骤2:获取库存锁inventoryLock.lock();try {// 快速失败if (MockData.getStock(productId) < 1) {System.out.println("用户 " + userId + " 库存不足,方案二无效");return;}// 执行所有操作MockData.addToCart(userId, productId, 1);MockData.decreaseStock(productId, 1);System.out.println("用户 " + userId + " 方案二下单成功!");MockData.clearCart(userId);} catch (Exception e) {log.error("异常", e);throw new RuntimeException(e);} finally {inventoryLock.unlock();}} catch (Exception e) {log.error("下单失败", e);} finally {cartLock.unlock();}}

请添加图片描述

总结

在并发系统的设计与优化中,死锁预防始终是确保系统稳定性的核心命题,我介绍的两种死锁的处理方式:

  1. "一刀切"的原子化方案
    通过全局锁强制串行化操作,牺牲了并发性能,以最简单的方式彻底消除死锁风险。这种"粗暴但可靠"的设计思路,特别适合对数据一致性要求极高、容错成本较大的业务场景,保证了基本的安全性。
  2. 精细化控制的顺序化策略
    访问资源顺序化,投机取巧的利用了业务场景优势,方案适合于对接口响应时间敏感的业务场景(下单抢购)。

实践启示录:

  • 没有银弹的解决方案:两种方案各有利弊,需根据业务特性进行取舍。高频小事务场景宜用原子化方案,长流程多步骤业务则更适合顺序化控制。
  • 死锁预防≠完全消除:即使采取最优策略,仍需通过监控(如JVM线程Dump分析)、日志埋点(死锁检测)、压力测试等手段持续验证系统稳定性。

优秀的设计永远是在理论模型与实际需求之间寻找精妙的平衡点。希望本文的分析能为你提供一些新的思路。

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

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

相关文章

极狐GitLab 17.9 正式发布,40+ DevSecOps 重点功能解读【四】

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料&#xff1a; 极狐GitLab 官网极狐…

黄昏时间户外街拍人像Lr调色教程,手机滤镜PS+Lightroom预设下载!

调色介绍 黄昏时分有着独特而迷人的光线&#xff0c;使此时拍摄的人像自带一种浪漫、朦胧的氛围 。通过 Lr 调色&#xff0c;可以进一步强化这种特质并根据不同的风格需求进行创作。Lr&#xff08;Lightroom&#xff09;作为专业的图像后期处理软件&#xff0c;提供了丰富的调色…

Spring Boot 项目中 Redis 常见问题及解决方案

目录 缓存穿透缓存雪崩缓存击穿Redis 连接池耗尽Redis 序列化问题总结 1. 缓存穿透 问题描述 缓存穿透是指查询一个不存在的数据&#xff0c;由于缓存中没有该数据&#xff0c;请求会直接打到数据库上&#xff0c;导致数据库压力过大。 解决方案 缓存空值&#xff1a;即使…

信息系统项目管理师--整合管理

信息系统项目管理师–整合管理

关于tomcat使用中浏览器打开index.jsp后中文显示不正常是乱码,但英文正常的问题

如果是jsp文件就在首行加 “<% page language"java" contentType"text/html; charsetUTF-8" pageEncoding"UTF-8" %>” 如果是html文件 在head标签加入&#xff1a; <meta charset"UTF-8"> 以jsp为例子&#xff0c;我们…

微服务的春天:基于Spring Boot的架构设计与实践

微服务的春天:基于Spring Boot的架构设计与实践 在如今的技术领域,微服务架构俨然成为了解决复杂系统开发与运维挑战的关键利器。作为一名资深运维和自媒体创作者,笔名Echo_Wish,我将深入探讨基于Spring Boot的微服务架构设计,结合实例代码说明观点,希望能为大家带来启发…

JVM参数调整

一、内存相关参数 1. 堆内存控制 -Xmx&#xff1a;最大堆内存&#xff08;如 -Xmx4g&#xff0c;默认物理内存1/4&#xff09;。-Xms&#xff1a;初始堆内存&#xff08;建议与-Xmx相等&#xff0c;避免动态扩容带来的性能波动&#xff09;。-Xmn&#xff1a;新生代大小&…

AVM 环视拼接 鱼眼相机

https://zhuanlan.zhihu.com/p/651306620 AVM 环视拼接方法介绍 从内外参推导IPM变换方程及代码实现&#xff08;生成AVM环视拼接图&#xff09;_avm拼接-CSDN博客 经典文献阅读之--Extrinsic Self-calibration of the Surround-view System: A Weakly... (环视系统的外参自…

【哇! C++】类和对象(三) - 构造函数和析构函数

目录 一、构造函数 1.1 构造函数的引入 1.2 构造函数的定义和语法 1.2.1 无参构造函数&#xff1a; 1.2.2 带参构造函数 1.3 构造函数的特性 1.4 默认构造函数 二、析构函数 2.1 析构函数的概念 2.2 特性 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中…

【五.LangChain技术与应用】【11.LangChain少样本案例模板:小数据下的AI训练】

深夜的创业孵化器里,你盯着屏幕上的医疗AI项目,手里攥着仅有的97条标注数据——这是某三甲医院心内科攒了三年的罕见病例。投资人刚刚发来最后通牒:“下周demo要是还分不清心肌炎和感冒,就撤资!” 这时你需要掌握的不是更多数据,而是让每个样本都变成会复制的孙悟空的毫毛…

2005-2019年各省城镇人口数据

2005-2019年各省城镇人口数据 1、时间&#xff1a;2005-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;地区、年份、城镇人口(万人) 4、范围&#xff1a;31省 5、指标解释&#xff1a;‌城镇人口是指居住在城市、集镇的人口&#xff0c;主要依据人群…

Anaconda 部署 DeepSeek

可以通过 Anaconda 环境部署 DeepSeek 模型&#xff0c;但需结合 PyTorch 或 TensorFlow 等深度学习框架&#xff0c;并手动配置依赖项。 一、Anaconda 部署 DeepSeek 1. 创建并激活 Conda 环境 conda create -n deepseek python3.10 # 推荐 Python 3.8-3.10 conda activate…

Python 面向对象高级编程-定制类

目录 __str__ __iter__ __getitem__ __getattr__ __call__ 小结 看到类似__slots__这种形如__xxx__的变量或者函数名就要注意&#xff0c;这些在Python中是有特殊用途的。 __slots__我们已经知道怎么用了&#xff0c;__len__()方法我们也知道是为了能让class作用于len()…

MCP与RAG:增强大型语言模型的两种路径

引言 近年来&#xff0c;大型语言模型&#xff08;LLM&#xff09;在自然语言处理任务中展现了令人印象深刻的能力。然而&#xff0c;这些模型的局限性&#xff0c;如知识过时、生成幻觉&#xff08;hallucination&#xff09;等问题&#xff0c;促使研究人员开发了多种增强技…

IDEA Generate POJOs.groovy 踩坑小计 | 生成实体 |groovy报错

一、无法生成注释或生成的注释是null 问题可能的原因&#xff1a; 1.没有从表里提取注释信息&#xff0c;修改def calcFields(table)方法即可 def calcFields(table) {DasUtil.getColumns(table).reduce([]) { fields, col ->def spec Case.LOWER.apply(col.getDataType().…

ue5.5崩溃报gpu错误快速修复注册表命令方法

网上已经有很多方法了&#xff0c;自己写了个regedit比处理dos批处理命令&#xff0c;启动时需要win 管理员身份拷贝后&#xff0c;将以下代码&#xff0c;保存为 run.bat格式批处理文件&#xff0c;右键鼠标&#xff0c;在弹出菜单中&#xff0c;选择用管理员身份运行。即可。…

能量石[算法题]

题目来源&#xff1a;第十五届蓝桥杯大赛软件赛省赛Java 大学 B 组&#xff08;算法题&#xff09; 可以参考一下&#xff0c;本人也是比较菜 不喜勿喷&#xff0c;求求求 import java.util.Scanner;​public class Main {public static void main(String[] args) {Scanner s…

马尔科夫不等式和切比雪夫不等式

前言 本文隶属于专栏《机器学习数学通关指南》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见《机器学习数学通关指南》 正文 统计概率的利剑&#xff1a;掌…

基于 STC89C52 的 8x8 点阵显示汉字

一、引言 在电子信息显示领域,汉字的直观呈现为信息传递带来极大便利。8x8 点阵虽显示空间有限,但通过合理设计,能够清晰展示一些常用、简单的汉字,丰富电子设备的交互界面。STC89C52 单片机作为一款经典且应用广泛的微控制器,以其成本低廉、易于开发的特性,成为驱动 8x…

二进制、八进制、十进制和十六进制间的转换(原理及工程实现)

在计算机科学和编程中&#xff0c;进制转换是一个非常重要的基础知识。无论是二进制、八进制、十进制还是十六进制&#xff0c;它们在不同的场景中都有广泛的应用。本文将详细介绍常用进制之间的转换方法&#xff0c;并附上C语言示例代码&#xff0c;帮助大家更好地理解和掌握这…