news 2026/10/19 11:30:27

并发编程核心:从本地锁到分布式锁的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
并发编程核心:从本地锁到分布式锁的完整实战指南

1. 并发问题的根源:三个看起来简单却天天踩坑的底层机制

做Java开发的朋友应该都有过这种经历:明明写了i++这种一行代码,压测一跑起来结果就不是期望值;明明加了synchronized,换了个分布式环境又发现锁根本不管用。高并发编程的难点不在于API怎么调用,而在于你根本看不见底层发生了什么。

这一篇我系统讲一讲从JVM内置锁到分布式锁的完整链路,包括我实际写过的代码、压测过的数据、坑过的线上事故,以及怎么一步步定位这些问题。

先说并发编程必须面对的三大问题:可见性、原子性、有序性。不想用教科书定义来讲,用大白话就三句话:

  • 可见性:线程A改了变量,线程B不一定能立刻看到这个修改。
  • 原子性:一个操作不可分割,比如i++其实包含"读取、加1、写回"三步,多线程下这三步会被打乱穿插。
  • 有序性:编译器、CPU为了性能会重排指令,代码写的是A在B前面,执行时B可能先跑了。

有一个反直觉的结论先说在前面:加锁并不是为了"阻塞线程",加锁真正解决的是这三类问题。阻塞只是加锁带来的副作用,不是目的。理解了这一点,后面你再看各种锁的实现就会非常通透。

以经典案例来说,我曾经在某电商类的模拟项目里复现过一个库存超卖bug:库存表stock初始值100,用10个线程并发执行stock--,结果跑完剩余库存是93到99不等,永远对不上。原因就是stock--这个操作不是原子的:每个线程同时读到100,然后各自减1,写回时互相覆盖。这种情况单机靠synchronized可以解决,但到了多实例部署,本地锁就不再有意义了,这就自然引出了分布式锁。

这一篇围绕"从并发三大问题到锁,从本地锁到分布式锁"这条主线展开,适合有一定Java基础、但没系统梳理过并发锁机制的同学,也适合已经在写业务代码但经常被并发bug困扰的开发者用来做排查思路参考。

2. 先从synchronized说起:JVM替你做的那些锁优化,以及两个反直觉的坑

2.1 synchronized的本质与锁升级路径

Java最基础的锁就是synchronized,在JDK 6之后它已经不是重量级锁的代名词了,JVM在内部做了大量优化,形成了从偏向锁到重量锁的升级路径。

synchronized底层依赖Monitor监视器锁,每次进入同步代码块都会执行monitorenter,退出时执行monitorexit。但JVM并不会上来就用重量级锁,因为重量级锁涉及操作系统内核态的系统调用,线程阻塞和唤醒的代价非常高。锁升级过程是这样的:

  • 偏向锁:只有一个线程访问时,在对象头Mark Word里记录线程ID,这个线程以后进入同步块不再做任何同步操作,性能与不加锁几乎一致。
  • 轻量级锁:有第二个线程也来竞争时,偏向锁撤销,升级为轻量级锁。这个阶段通过CAS自旋获取锁,不阻塞线程。
  • 重量级锁:自旋超过阈值或自旋线程数过多时,升级为重量级锁,线程进入阻塞队列,依赖操作系统互斥量实现。

实际开发中需要注意的一点:偏向锁在JDK 15开始默认被禁用,后续版本逐渐废弃。如果你的应用运行在JDK 15+,偏向锁相关的调优参数已经没用了。

2.2 synchronized的两个常见误用

第一个误区:以为synchronized加在方法上就能锁住"这段业务逻辑"。

实际上synchronized锁的是对象,不是方法。加在实例方法上锁的是当前实例this,加在静态方法上锁的是Class对象。来看一个常见翻车案例:

public class StockService { public synchronized void deduct() { // 扣减库存逻辑 } }

如果这个StockService在Spring容器中是单例,没问题。但如果被new出了多个实例,比如在某些定时任务里每次手动创建,那么每个实例锁的对象都不一样,锁就完全失效了。我当时排查线上问题的时候,先用jstack抓线程状态,发现多个线程各自进入同步方法互不阻塞,第一反应就是"实例是不是多份"。

第二个误区:synchronized锁不住分布式环境。

这就是前面说的天然边界问题。A机器上的线程锁住了A机器上的对象,B机器上根本感知不到这个锁的存在。很多团队在单体应用阶段依赖synchronized很顺手,一旦改成多实例部署就出问题,本质就是没有理解锁的粒度边界在哪里。

2.3 锁对象选择的一个实用原则

选锁对象有个经验法则:锁的范围应该等于共享资源的范围。操作同一个库存商品的扣减逻辑,锁就该是这个商品的维度,比如用商品ID作为锁粒度;如果是扣用户余额,锁的用户维度。锁粒度太粗(整表锁)会严重降低吞吐量,锁粒度太细(比如每次new一个Object当锁)等于没锁。

用一段生产上常见的写法说明:

public class InventoryService { private final ConcurrentHashMap<String, Object> lockMap = new ConcurrentHashMap<>(); public void deduct(String productId, int count) { // 每个商品一把锁,互不影响 Object lock = lockMap.computeIfAbsent(productId, k -> new Object()); synchronized (lock) { // 查询库存、校验、扣减、写日志 doDeduct(productId, count); } } }

这种方式称为"分段锁"或"细粒度锁",在高并发场景下性能远优于直接锁整个方法。ConcurrentHashMap本身线程安全,computeIfAbsent保证同一商品ID只会创建一把锁对象,不同商品之间的锁互不干扰,吞吐量会高很多。

3. JUC锁工具箱:ReentrantLock、读写锁与StampedLock的适用场景选择

3.1 ReentrantLock和synchronized怎么选

Redis读者可能听过Java并发包(JUC——Java Util Concurrent)里的ReentrantLock。它是基于AQS(AbstractQueuedSynchronizer)实现的,这个AQS是整个JUC锁体系的基石,核心是一个volatile状态变量加CLH等待队列。

ReentrantLock相比synchronized几个显著的能力差异是:

  • 可中断:lockInterruptibly()可以在等待锁的过程中响应中断。
  • 可超时:tryLock(timeout, TimeUnit.SECONDS)等待超过指定时间就放弃,避免死锁。
  • 可公平:构造时传入fair=true可以启用公平锁。
  • 多个条件变量:newCondition()可以创建多个等待队列,比synchronized的wait/notify更灵活。

实际生产中的一个判断依据是:如果只是简单互斥,优先synchronized,因为写法简单、JVM层面还在持续优化;如果需要超时控制、公平性控制,或者需要多个条件队列,那就选ReentrantLock。强锁(tryLock超时)在分布式锁场景里也经常用到,比如本地缓存刷新时多线程竞争更新。

不过有一点要特别注意:必须在finally里释放锁。synchronized是JVM自动释放,ReentrantLock可不会,忘记unlock的后果就是死锁,而且这种死锁用jstack一看全是WAITING状态,排查起来很头疼。

3.2 读写锁与缓存场景

读多写少的场景最典型的是缓存。ReentrantReadWriteLock维护一个读锁和一个写锁,读锁之间可以共享,读锁和写锁互斥,写锁和写锁互斥。这种"多读单写"的设计能让读并发能力成倍提升。

但ReadWriteLock有一个隐蔽的坑:写锁饥饿。如果有大量读线程持续持有读锁,写线程可能长时间得不到执行。ReentrantReadWriteLock里非公平模式下写锁获取概率也不高,极端情况下写线程会一直等待。长事务和高频读同时存在时,可能出现读线程源源不断进来、写线程始终抢不到锁的情况。

针对这个问题,JDK 8引入的StampedLock提供了乐观读功能。乐观读不实际加锁,先读,读完校验版本号(stamp),如果期间没有写操作就直接用数据,如果有写操作就升级为读锁重新读。适合读多写少且读操作较快的场景。使用时要小心,StampedLock不支持重入,而且用不好容易产生CPU占用问题。

3.3 JUC底层AQS的要义

AQS是一条抽象队列同步器,JUC里的大部分锁工具都基于它实现。其核心是state(volatile int)加FIFO等待队列。以ReentrantLock为例,lock时CAS尝试把state从0改成1,成功就拿到锁;失败则进入等待队列。重入时state累加,释放时递减到0才算完全释放。

我最初学AQS的时候觉得这个东西太抽象,后来写了一个自定义的限流器才真正理解它的设计。实际使用中,你基本不需要自己实现AQS,但理解它的模型对排查锁问题非常有用。比如用jstack看到大量线程阻塞在AbstractQueuedSynchronizer$ConditionObject时,就知道它们是在条件等待队列上等待信号,可以用signal()唤醒来排查。

4. JVM锁之外的并发利器:从原子类到并发容器的工程选择

4.1 AtomicXXX与CAS为什么会ABA

Java并发包除了锁还有一套无锁方案:CAS(Compare And Swap)。AtomicInteger、AtomicLong、AtomicReference这些原子类都基于Unsafe类的CAS原语实现,不需要加锁也能保证原子性。

CAS的核心思路是:更新变量前先比对我读到的值和内存当前值是否一致,一致才更新,否则重试。这看起来很美,但存在一个经典问题——ABA问题:线程A读到值是1,线程B把它改成2再改成1,线程A再次CAS时发现还是1,以为没人改过。对于某些场景(如无锁栈),ABA会导致意想不到的数据错乱。

解决方案是使用AtomicStampedReference或AtomicMarkableReference,它们额外维护版本号,CAS时同时比较值和版本号。

实际工程中CAS有另外一个性能隐患:高竞争下大量线程CAS失败会导致频繁自旋,CPU飙升。所以如果竞争非常激烈,原子类的性能可能反而不如锁。我压测过8线程下AtomicLong和LongAdder的差异,LongAdder在高并发下胜出非常多。原因是LongAdder把单一热点value拆成了多个slot,线程分散到不同slot累加,最后sum()时再汇总。所以计数场景现在我的默认选择是LongAdder而不是AtomicLong。

4.2 并发容器的选型原则

很多人提到高并发就想到ConcurrentHashMap,但它也有使用边界。ConcurrentHashMap的弱一致性迭代器(entrySet、size等方法)不保证实时一致,如果你需要"读到的数据一定是当前最新的"这种强一致性,它并不合适。

不同并发场景下的容器选择原则:

场景推荐容器理由
高并发KV读写ConcurrentHashMap分段锁/ CAS + 红黑树优化,读写并发高
生产者消费者队列ArrayBlockingQueue / LinkedBlockingQueue自带阻塞与唤醒机制
多线程安全计数器LongAdder / AtomicLong无锁或分散热点,吞吐量高
有序不重复集合ConcurrentSkipListSet基于跳表,支持高并发有序读写
延迟任务调度DelayQueue按过期时间排序,配合ScheduledThreadPoolExecutor使用

当你选择容器的时候,先回答三个问题:数据能不能丢失?一致性要求多高?读写比例是多少?答案直接决定容器选择。比如日志采集场景可以允许少量丢,用ConcurrentLinkedQueue就可以了;但订单状态流转不能丢,必须用有界阻塞队列或数据库行锁兜底。

4.3 ThreadLocal与线程池搭配的隐患

另一个高频坑是ThreadLocal在线程池里使用。ThreadLocal是按线程维度存储变量的,线程池里的线程会复用,如果不清理,上一次请求留下的数据会泄漏到下一次任务中。严重一点如果是引用了大对象还可能引发内存泄漏。

我的处理习惯:

try { // 业务逻辑,使用 ThreadLocal 保存上下文 } finally { contextThreadLocal.remove(); }

在finally里remove几乎是必须的规范。真实线上排查过一个诡异问题:同一个线程处理不同请求时,偶尔能看到上一个请求的用户ID串进来,最后定位就是ThreadLocal没有清理。

5. 从本地锁到分布式锁:到底锁不住的东西是什么

5.1 为什么本地锁到了分布式环境就失效

把前面几节串起来,本地锁(synchronized、ReentrantLock、AQS)锁的是JVM进程内的对象和线程。但到了分布式部署,多个服务实例各自跑在不同的JVM里,库存数据、订单数据存在共享的数据库或缓存里,某个实例内的锁管不到其他实例,这就是分布式锁问题的根源。

一个最直观的例子:一个商品有100件库存,服务部署了3个实例,每个实例都执行synchronized(deduct)扣减。假设同时来了3个请求,每个请求恰好打到不同实例,3个线程同时读到库存100并各自减1,最终可能得到97而不是99。这三个本地锁各锁各的,谁也不认识谁。

分布式锁的本质:需要一个所有实例都能访问到的、带有互斥能力的公共存储,在这个公共存储上记录"谁持有锁",其他实例来抢锁时看到有人持有了就等待或失败。

5.2 分布式锁的设计要求清单

设计一个可靠的分布式锁,工程上至少要满足以下几条:

  • 互斥性:任何时刻只有一个客户端能持有锁。
  • 死锁防护:持锁客户端崩溃后,锁必须能在一定时间后自动释放,不能永久死锁。
  • 可重入性(视场景):同一个客户端可以重复获取同一把锁。
  • 锁超时与自动续期:业务执行时间超过锁过期时间时,需要自动续期,避免业务未结束锁就失效。
  • 安全释放:只能释放自己持有的锁,不能误删别人的锁。
  • 高可用:锁服务本身不能成为单点。

这些要求看起来不算多,实际做起来坑非常多。后面讲Redis实现时会逐个对应。

5.3 选型路径:从数据库锁到Redis再到ZK

分布式锁的常见实现路径有三条:数据库锁、Redis锁、ZooKeeper/etcd锁。

  • 数据库锁:利用数据库唯一索引或for update行锁,实现简单,但性能较差,且依赖数据库可用性。
  • Redis锁:基于单节点SETNX或Redisson封装,性能高,是互联网业务的主流选择。
  • ZooKeeper锁:利用临时顺序节点和事件监听,可靠性高,能自动释放,但部署维护成本高、性能低于Redis。

之后我会用大量篇幅拆解Redis方案——因为它是目前工业界应用最广泛、方案最成熟、但也是最容易用错的一种。

6. Redis分布式锁实战:SETNX手写到Redisson的完整进化

6.1 用SETNX实现分布式锁的三个迭代版本

第一版:最简单的SETNX。

public boolean tryLock(String key, String value, long expireMillis) { // 返回true表示获取锁成功 return redisTemplate.opsForValue().setIfAbsent(key, value, expireMillis, TimeUnit.MILLISECONDS); }

setIfAbsent对应Redis的SET key value NX EX,原子地实现"不存在才设置且带过期时间"。这是目前最基础的Redis分布式锁写法,解决了互斥性和基本死锁防护(过期时间兜底)。

第二版:加入防误删。

只设置过期时间还不够,如果线程A持有锁执行较久,锁过期自动释放后,线程B拿到锁开始执行;此时线程A业务结束,执行删除锁操作,会把线程B持有的锁误删掉,B的锁形同虚设,两个线程同时进入临界区,超卖问题立刻复现。

正确做法是删除前比较value是否是自己写入的那个唯一值:

public boolean releaseLock(String key, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key), value); return result != null && result == 1L; }

用Lua脚本保证"比对+删除"两步的原子性。这个value就是每次获取锁时生成的UUID或者业务请求ID,作为锁的唯一标识。

第三版:WatchDog自动续期。

第二个问题:锁过期时间设多少合适?设短了,业务没跑完锁没了,并发控制失效;设长了,持有锁的线程崩溃后其他线程要等很久。

Redisson的WatchDog机制是每lockLeaseTime / 3时间间隔自动续期一次(默认leaseTime是30秒,每10秒续期到30秒)。这样只要业务没结束,锁就一直续期;业务结束释放锁后,续期任务也随之取消。这个机制不是Redisson私有,但很多团队手写不完整——一个标准的续期调度器需要:启动一个后台定时任务,检查锁是否还存在且当前持有者是自己,是则延长过期时间,业务结束时关闭定时任务。

6.2 Redisson的使用与可重入实现

实际工程中不太建议自己手写上面这些细节,成熟方案Redisson已经把这些都处理好了:

RLock lock = redissonClient.getLock("inventory:lock:" + productId); try { // 尝试加锁,最多等待5秒,锁自动过期30秒,WatchDog自动续期 if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }

Redisson的RLock是可重入的,同一个线程可以多次加锁同一把锁,内部用Hash结构维护线程标识和重入次数。对比手写SETNX,Redisson帮你处理了续期、防误删、重入和公平锁。

有一点需要注意:tryLock(waitTime, leaseTime, unit)传入leaseTime后,WatchDog续期机制就不会生效,锁在指定时间后必定释放。如果需要续期,使用不带leaseTime的重载方法,只在等待时间内尝试加锁,锁的自动过期时间由默认值控制并自动续期。

6.3 Redis主从切换引发的锁丢失问题

在实际生产架构中,Redis通常是主从模式。这里有一个隐蔽的风险:客户端A在主节点成功写入锁key,同步给从节点这一步尚未完成,主节点发生故障,从节点晋升为主节点,但新主节点没有这个锁key的记录。此时客户端B来加锁会发现这个key不存在,加锁成功,两个客户端同时持有锁,互斥失效。

这里需要引入Redis官方提到的RedLock算法:向多个独立的Redis节点同时加锁,超过半数成功才算获取锁成功。但RedLock争议很大,我的看法是:如果业务对互斥性极度敏感(比如资金操作、扣库存),最好别把命运压在Redis主从切换上,用ZooKeeper更稳妥;如果是常规业务容忍极小概率并发超量,RedLock或单节点+合理监控可以接受。

我在生产里见过一个折中做法:Redis锁负责高效互斥,同时在最终持久化层(数据库)做乐观锁或唯一索引兜底,双重校验。也就是"缓存层锁加快失败,数据库层锁保证最终一致"。这套方案虽然多一次DB校验,但能把损失控制在极低概率内。

7. ZooKeeper分布式锁:临时顺序节点为什么更可靠

7.1 基于ZK原生API实现分布式锁

ZooKeeper实现分布式锁的思路和Redis完全不同。它利用了ZooKeeper的两个特性:临时节点(客户端断开连接自动删除)和顺序节点(节点名自增序号)。

实现逻辑大致是:

  1. 在指定锁路径下创建一个临时顺序节点,比如/locks/lock_000000001。
  2. 获取当前路径下所有子节点,判断自己是不是序号最小的那个。
  3. 如果是,获得锁;如果不是,对前一个节点注册监听。
  4. 前一个节点被删除(锁释放或客户端崩溃),自己被唤醒,重新判断。

因为每个客户端创建的节点是有序的,就形成了一条FIFO的等待队列,天然支持公平锁,且不会有"惊群效应"(每个节点只监听前一个节点)。

7.2 ZK锁对比Redis锁的本质差异

ZK锁的核心优势是:临时节点依赖客户端会话,一旦客户端崩溃或网络断开,会话失效,节点自动删除,锁自动释放,不会出现Redis锁那种"主从切换丢锁"问题。

性能上ZK锁平均耗时通常高于Redis锁,因为它有磁盘同步和节点监听回调的成本。但可靠性要求很高的场景,比如秒杀系统的库存扣减、分布式任务调度中的唯一执行者选举,我更倾向于ZK方案。

用Apache Curator框架实现ZK分布式锁非常简洁:

InterProcessMutex lock = new InterProcessMutex(client, "/locks/inventory_" + productId); try { if (lock.acquire(5, TimeUnit.SECONDS)) { // 业务处理 } } finally { lock.release(); }

Curator还提供了InterProcessReadWriteLock(读写锁)、InterProcessSemaphoreMutex(不可重入锁),可以根据场景选择。

7.3 选型建议:别迷信某一种方案

我自己的选型参考表:

维度Redis锁ZooKeeper锁
性能高,微秒级中,毫秒级
可靠性依赖部署模式,主从切换有丢锁风险高,临时节点自动释放
复杂度低,Redis通常已有高,需要维护额外集群
公平性默认不公平,需额外实现天然公平
适用场景常规业务限流、缓存防击穿、幂等控制库存扣减、任务调度、分布式事务协调

如果Redis已经是你系统的基础组件,优先用Redisson封装好的锁;如果业务对一致性和可靠性极其敏感,ZK(或etcd)是更稳妥的选择。

8. 生产级落地:事务边界、锁粒度与性能压测的关键细节

8.1 锁与事务执行顺序

这是分布式锁实践里我最想强调的一点:锁必须在事务提交之后再释放,而且锁的释放最好放在事务提交方法的外层。

常见错误写法:

@Transactional public void deductStock(String productId, int count) { // 在这个方法内部获取分布式锁 RLock lock = redisson.getLock("lock:" + productId); lock.lock(); try { // 扣库存SQL } finally { lock.unlock(); } }

问题在于:@Transactional的方法执行结束时,事务可能尚未提交。如果先unlock再提交事务,另一个线程在锁释放后立刻查到旧库存数据,又做了一次扣减,等于锁白加了。

正确姿势是让锁和事务边界分离:

public void deductStock(String productId, int count) { RLock lock = redisson.getLock("lock:" + productId); lock.lock(); try { // 手动控制事务范围,确保事务提交成功后再释放锁 doDeductWithTransaction(productId, count); } finally { lock.unlock(); } } @Transactional public void doDeductWithTransaction(String productId, int count) { // 真正的扣减逻辑 }

这样事务提交完了,锁才释放,其他线程才能看到新数据。这里再补充一个细节:Spring的@Transactional默认是方法内代码执行完马上提交事务,所以如果你在事务方法内直接操作锁,一定要把锁放到事务方法外层的调用者那一层。

8.2 锁粒度设计的三个案例

锁粒度直接影响系统吞吐量。我用三类业务来说:

  • 库存扣减:按productId加锁,不同商品不互相影响。极端热点商品可再拆成多级库存桶。
  • 订单支付幂等:按orderId加锁,同一订单串行,不同订单并发。
  • 用户余额变更:按userId加锁,粒度足够细且安全。

尽量别做"全局锁"。我见过一个项目为了防止所有的写操作并发冲突,直接对一把全局锁加锁,结果压测QPS从预期的2000掉到50,排查半天发现是锁粒度太粗。

8.3 压测验证锁的正确性

分布式锁写完不压测等于没写。我的压测三步走:

  1. 单实例JUnit并发测试:用CountDownLatch模拟多线程并发执行,验证互斥性。
  2. 多实例部署测试:在本机启动多个应用实例或端口不同的多个进程,通过nginx或直连不同地址压测,验证分布式锁是否真正生效。
  3. 故障注入测试:杀掉持有锁的实例,观察锁能否在预期时间内自动释放,其他实例能否正常获取锁。

压测指标看两个:正确性(最终库存是否准确)和性能(QPS、平均响应时间、P99)。如果正确性没问题但性能极低,优先检查锁粒度和锁等待时间设置。

一个我在实战中总结出来的结论:分布式锁能用好,一半靠锁本身的实现,一半靠事务边界、锁粒度、超时时间这些业务侧参数。锁只是工具,设计合理才能发挥价值。

8.4 死锁与锁超时的检测手段

分布式环境下死锁不像单机那么好抓。我常用的排查手段:

  • 通过Redis的HGETALL查看锁key的持有者信息和重入次数。
  • 通过ZK的节点列表看临时顺序节点卡在哪个位置。
  • 通过日志埋点记录"加锁时间、获得锁耗时、释放锁耗时",用监控平台看分布。
  • 对每个关键锁加ThreadPoolExecutor任务超时告警,超过阈值就告警人工介入。

还有一条:锁key的命名空间一定要规范,比如{业务}:lock:{资源ID}。否则多业务混用同一Redis集群时,key冲突排查会非常痛苦。

9. 从锁机制到无锁设计:我在实战中总结的并发优化路线

9.1 锁不是唯一的答案

做高并发时间长了会发现,很多时候不应该先想"加什么锁",而是先想"能不能不加锁"。

以扣库存为例,如果允许超卖,完全可以用Redis的DECR原子操作直接扣减;如果要求精确库存,可以用数据库乐观锁UPDATE ... SET stock = stock - #{count} WHERE stock >= #{count},配合影响行数判断。这种"原子性由底层实现,而不是互斥锁"的思路,往往比锁更高效。

9.2 乐观锁在数据库层的落地

在数据库层用乐观锁解决并发扣减是分布式锁外的常用手段:

UPDATE inventory SET stock = stock - #{count}, version = version + 1 WHERE product_id = #{productId} AND stock >= #{count} AND version = #{version}

影响行数大于0说明扣减成功,等于0说明并发冲突,业务方决定重试或返回失败。这种方案的好处是不用额外维护分布式锁基础设施,缺点是冲突频繁时重试会导致请求延迟增加。

9.3 串行化+异步化的极端思路

某个秒杀系统里我见过一种思路:把秒杀请求先全部写入MQ削峰,消费端串行处理,实际并发度被强制压到1。这种做法牺牲了部分吞吐但彻底避开锁竞争。它适用写的低吞吐但必须极稳的场景。

这给我们一个启示:高并发不是"越高越要上复杂技术",而是要根据真实业务约束选择最合适的策略。有时候把并发模型改串行反而比用一堆锁更简单、更可靠。

结束前的一点个人经验

最后分享一个实战中反复验证过的观点:判断系统是否需要引入分布式锁,先看你的服务是否真的会部署多个实例,再看共享资源是否会被多个实例同时写。单实例阶段引入分布式锁是过度设计,多实例阶段还只用synchronized那就是事故隐患。

如果真的要用分布式锁,建议直接把Redisson或Curator这套成熟方案落地上手,不要自己从SETNX开始造轮子。手写分布式锁能帮你理解原理,拿来当生产方案则太容易遗漏细节。我在某模拟项目里分阶段观察过:手写方案在上线后半年内踩到了续期失效、主从切换丢锁、误删他人锁三个问题,而这些问题在Redisson中都早已被处理得很完善。

从这个角度看,Java并发编程的学习路径也很清晰:先用好synchronized和JUC的锁工具理解本地互斥,再理解分布式环境下公共存储的互斥设计,最后再根据一致性和性能要求做技术选型。这条路径走通一遍,你对"锁"的理解就不再停留在API调用层面,而是真正建立起一套判断并发问题的工程思维。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 11:29:13

从零搭建OAuth2.0认证授权中心:IdentityServer4实战教程

先说一下&#xff0c;IdentityServer4 这套东西在 .NET 生态里虽然已经进入了“维护模式”&#xff0c;但存量项目实在太多&#xff0c;而且它背后的 OAuth 2.0 / OpenID Connect 思想完全没过时。我之前帮几家公司接过老的 IdentityServer4 项目&#xff0c;也自己从零搭过整套…

作者头像 李华
网站建设 2026/10/19 11:28:12

密码破解分析系统:Seq2Seq/PITG建模与guessrate评估

简介&#xff1a;一套基于 Python 语言编写的密码破解分析系统源码&#xff0c;面向计算机安全方向的研究者与机器学习爱好者&#xff0c;聚焦如何借助深度学习模型剖析密码破解数据、发现解密规律并提升破解效率。压缩包内共 74 个文件&#xff0c;以 60 个 Python 脚本为主&a…

作者头像 李华
网站建设 2026/10/19 11:28:05

工业智能体落地实践:四层能力结构与多智能体协作技术解析

1. 从一纸征稿通知说起&#xff1a;工业智能体到底在解决什么问题 看到“工业智能体”这个主题专刊的征稿通知&#xff0c;我第一反应不是“又来了一个热词”&#xff0c;而是想起过去两年在几个制造现场做数字化项目时反复遇到的一个尴尬局面&#xff1a;数据采了一大堆&#…

作者头像 李华
网站建设 2026/10/19 11:27:40

Python三维点云车辆检测:注意力机制与C++/CUDA算子实战拆解

简介&#xff1a;面向自动驾驶、机器人导航等对三维感知有明确要求的场景&#xff0c;压缩包内以Python实现了基于注意力机制的三维点云车辆目标检测算法&#xff0c;适合已有深度学习基础、希望研究点云特征提取与注意力机制结合的开发者或工程师。压缩包共347个文件&#xff…

作者头像 李华
网站建设 2026/10/19 11:26:20

S7-1200主从PLC通信实战:PUT/GET指令详解与配置

一条产线上你总会遇到这种情况&#xff1a;三台设备各用一台 S7-1200 控制&#xff0c;但设备之间要联动。1 号机停了&#xff0c;后面两台必须跟着停&#xff1b;1 号机还要把配方号、运行模式同步给后面两台&#xff1b;每台的产量和故障状态又得汇总回中控。有人图省事&…

作者头像 李华
网站建设 2026/10/19 11:25:50

安全代理域名如何被钓鱼攻击滥用?信任链检测与防御实战

最近在复盘一批钓鱼样本时&#xff0c;有个现象特别值得拿出来说。攻击者越来越不爱用那种一看就很假的仿冒域名了&#xff0c;反而开始盯上那些带着“安全”字眼、经过合法代理机制包装的域名&#xff0c;专门吃用户和检测系统对“安全域名”的信任。这种攻击模型&#xff0c;…

作者头像 李华