前端转全栈(Java 后端)必须要知道的:开发中的锁机制与分布式并发控制

Start

很多前端同学在转向全栈,尤其是深入 Java 后端开发时,会发现一个明显的变化:前端更多关注的是页面交互、组件状态、接口调用和用户体验,而后端除了要处理业务逻辑,还必须面对一个更底层、更复杂的问题------并发

在真实的后端系统里,同一时间可能有成百上千个请求同时进来。多个用户可能同时抢购同一件商品,多个服务实例可能同时执行同一个定时任务,多个线程可能同时修改同一份数据。如果没有合适的并发控制,就可能出现库存超卖、重复下单、数据覆盖、缓存击穿、任务重复执行等问题。

而"锁",就是 Java 后端解决并发问题时非常核心的一类工具。

从单机应用中的 synchronizedReentrantLock,到数据库里的悲观锁、乐观锁、行锁,再到分布式系统中的 Redis 分布式锁、Zookeeper 锁、etcd 锁,锁的使用场景越来越广,复杂度也越来越高。尤其是在现在微服务、多实例部署、分布式架构越来越常见的背景下,只理解 Java 线程锁已经远远不够,还需要知道不同锁的适用范围、实现原理、常见坑点以及在 Spring 生态中有哪些现成工具可以使用。

这篇文章会站在前端转全栈 Java 后端开发者的视角,用尽量通俗的方式,系统梳理后端开发中常见的各种锁,包括:

  • JVM 内部锁;
  • 数据库锁;
  • 乐观锁和悲观锁;
  • Redis 分布式锁;
  • Zookeeper、etcd 分布式协调锁;
  • 定时任务锁;
  • Spring 中和锁相关的注解与组件;
  • 实际业务中如何选型。

读完之后,你不一定会立刻成为并发专家,但至少能够明白:什么时候该加锁,什么时候不该加锁,用什么锁,以及加锁时最容易踩哪些坑。

下面按"单机 JVM 内锁 → 数据库锁 → 分布式锁 → Spring 中可用注解/组件 → 选型建议"来讲。


1. 为什么后端开发需要锁?

锁的核心作用是:

在并发场景下,保证同一时刻只有一个线程、一个进程或一个服务实例可以操作某个共享资源。

常见问题:

  • 商品库存超卖;
  • 用户重复提交订单;
  • 定时任务多节点重复执行;
  • 多线程更新同一个对象导致数据错乱;
  • 多个服务实例同时处理同一笔业务;
  • 缓存击穿时大量请求同时查数据库。

锁大体可以分为几类:

分类维度 类型
按作用范围 JVM 内锁、数据库锁、分布式锁
按加锁策略 悲观锁、乐观锁
按读写模式 独占锁、共享锁、读写锁
按可重入性 可重入锁、不可重入锁
按公平性 公平锁、非公平锁
按实现方式 synchronized、ReentrantLock、CAS、DB、Redis、Zookeeper、etcd 等

2. JVM 内部锁

JVM 内部锁只在单个 Java 进程内有效

如果你的应用只部署了一个实例,那么 JVM 锁就够用。

如果应用是多实例部署,比如 3 台机器上都跑了同一个服务,那么 JVM 锁就不够了。


2.1 synchronized

synchronized 是 Java 最基础的内置锁。

可以修饰:

java 复制代码
public synchronized void method() {
    // 临界区
}

或者锁代码块:

java 复制代码
synchronized (this) {
    // 临界区
}

也可以锁类对象:

java 复制代码
synchronized (OrderService.class) {
    // 类级别锁
}

特点

  • JVM 原生支持;
  • 可重入;
  • 自动加锁、释放锁;
  • 不需要手动 unlock;
  • 适合简单同步场景;
  • 无法设置超时时间;
  • 无法中断等待锁的线程;
  • 无法实现公平锁。

适用场景

单 JVM 内,对共享变量、共享对象进行简单并发保护。

例如:

java 复制代码
private int count = 0;

public synchronized void increment() {
    count++;
}

2.2 ReentrantLock

ReentrantLock 是 JUC 包中的显式锁。

java 复制代码
private final ReentrantLock lock = new ReentrantLock();

public void doSomething() {
    lock.lock();
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

特点

相比 synchronized 更灵活:

  • 可重入;
  • 支持公平锁;
  • 支持尝试加锁;
  • 支持超时加锁;
  • 支持可中断加锁;
  • 支持多个 Condition 条件队列。

例如超时加锁:

java 复制代码
if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        // 拿到锁后执行业务
    } finally {
        lock.unlock();
    }
} else {
    // 3 秒内没拿到锁
}

公平锁和非公平锁

java 复制代码
// 非公平锁,默认
new ReentrantLock();

// 公平锁
new ReentrantLock(true);

公平锁按照等待顺序获取锁,避免饥饿,但性能一般比非公平锁差。

适用场景

需要更灵活的锁控制时使用,比如:

  • 尝试加锁;
  • 超时放弃;
  • 可中断等待;
  • 多条件队列。

2.3 ReentrantReadWriteLock

读写锁把锁拆成两种:

  • 读锁:共享锁,多个线程可以同时读;
  • 写锁:独占锁,同一时间只能一个线程写。
java 复制代码
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();

public String readData() {
    readLock.lock();
    try {
        return data;
    } finally {
        readLock.unlock();
    }
}

public void writeData(String newData) {
    writeLock.lock();
    try {
        data = newData;
    } finally {
        writeLock.unlock();
    }
}

特点

  • 读读不互斥;
  • 读写互斥;
  • 写写互斥;
  • 适合读多写少场景。

适用场景

例如本地缓存:

java 复制代码
Map<String, Object> cache = new HashMap<>();

读操作很多,写操作较少,可以使用读写锁。


2.4 StampedLock

StampedLock 是 JDK 8 引入的锁,比 ReentrantReadWriteLock 更激进。

它支持:

  • 写锁;
  • 悲观读锁;
  • 乐观读。

乐观读示例:

java 复制代码
private final StampedLock stampedLock = new StampedLock();

private int x;
private int y;

public int distance() {
    long stamp = stampedLock.tryOptimisticRead();

    int currentX = x;
    int currentY = y;

    if (!stampedLock.validate(stamp)) {
        stamp = stampedLock.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }

    return currentX * currentX + currentY * currentY;
}

特点

  • 性能高;
  • 支持乐观读;
  • 适合读多写少;
  • 不可重入;
  • 使用复杂,容易出错。

适用场景

读多写少,并且对性能要求比较高的场景。


2.5 CAS 和原子类

CAS,全称 Compare And Swap,比较并交换。

它不是传统意义上的锁,而是一种无锁并发控制技术

Java 中常见类:

java 复制代码
AtomicInteger
AtomicLong
AtomicReference
AtomicBoolean
LongAdder
LongAccumulator

示例:

java 复制代码
AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();

底层大致逻辑:

java 复制代码
如果当前值 == 预期值:
    更新为新值
否则:
    更新失败,重试

特点

  • 无锁;
  • 性能好;
  • 适合简单计数、状态切换;
  • 高并发下可能自旋消耗 CPU;
  • 存在 ABA 问题。

ABA 问题可以用:

java 复制代码
AtomicStampedReference
AtomicMarkableReference

来解决。

适用场景

例如:

  • 计数器;
  • 状态标记;
  • 简单库存扣减;
  • 本地限流计数。

2.6 Semaphore

Semaphore 不是严格意义上的互斥锁,它是信号量,用来控制并发数量。

java 复制代码
Semaphore semaphore = new Semaphore(10);

public void handle() throws InterruptedException {
    semaphore.acquire();
    try {
        // 最多允许 10 个线程同时进来
    } finally {
        semaphore.release();
    }
}

适用场景

  • 控制接口并发;
  • 控制资源池访问;
  • 限制同时处理的任务数量。

例如:最多允许 10 个线程同时上传文件。


2.7 CountDownLatch、CyclicBarrier、Phaser

这些也不是锁,而是线程协作工具。

CountDownLatch

一个线程等待多个线程完成:

java 复制代码
CountDownLatch latch = new CountDownLatch(3);

latch.countDown();

latch.await();

CyclicBarrier

多个线程互相等待,到齐后一起继续:

java 复制代码
CyclicBarrier barrier = new CyclicBarrier(5);
barrier.await();

Phaser

更灵活的多阶段同步器。


3. 乐观锁和悲观锁

这是开发中非常重要的一组概念。


3.1 悲观锁

悲观锁的思想是:

我认为别人一定会来抢,所以我先加锁。

典型实现:

  • synchronized
  • ReentrantLock
  • 数据库 select ... for update
  • Redis 分布式锁
  • Zookeeper 分布式锁

适合场景

  • 冲突概率高;
  • 数据一致性要求高;
  • 不允许失败重试;
  • 临界区较短。

3.2 乐观锁

乐观锁的思想是:

我认为一般不会冲突,提交更新时再检查有没有被别人改过。

常见实现:

  • CAS;
  • 数据库 version 字段;
  • 数据库 update 条件判断。

数据库乐观锁示例:

sql 复制代码
update product
set stock = stock - 1,
    version = version + 1
where id = 100
  and stock > 0
  and version = 10;

如果返回影响行数为 1,说明扣减成功。

如果返回影响行数为 0,说明数据被别人改过,需要重试或者提示失败。

表结构示例

sql 复制代码
create table product (
    id bigint primary key,
    stock int not null,
    version int not null
);

适合场景

  • 冲突概率较低;
  • 读多写少;
  • 可以接受失败重试;
  • 不想长时间占用锁。

4. 数据库锁

在后端系统中,数据库锁非常常用,尤其是和事务结合使用。

以 MySQL InnoDB 为例。


4.1 行锁

行锁锁住某一行数据。

例如:

sql 复制代码
select * from product where id = 1 for update;

这会对 id = 1 的记录加排他锁。

其他事务如果也想修改这行数据,就要等待。

Java 中一般这样用

java 复制代码
@Transactional
public void deductStock(Long productId) {
    Product product = productMapper.selectForUpdate(productId);

    if (product.getStock() <= 0) {
        throw new RuntimeException("库存不足");
    }

    productMapper.updateStock(productId, product.getStock() - 1);
}

SQL:

sql 复制代码
select * from product where id = #{productId} for update;

注意:

for update 必须放在事务里才有意义。

也就是方法上要有:

java 复制代码
@Transactional

4.2 表锁

表锁会锁住整张表。

例如:

sql 复制代码
lock tables product write;

表锁粒度大,并发性能差,一般业务开发中不常主动使用。


4.3 共享锁和排他锁

共享锁,S 锁

多个事务可以同时读。

MySQL 8 中:

sql 复制代码
select * from product where id = 1 for share;

旧版本中:

sql 复制代码
select * from product where id = 1 lock in share mode;

排他锁,X 锁

一个事务拿到排他锁后,其他事务不能修改,也不能加排他锁。

sql 复制代码
select * from product where id = 1 for update;

4.4 间隙锁 Gap Lock

间隙锁锁的是索引记录之间的间隙。

比如表中有 id:

text 复制代码
1, 5, 10

如果查询范围:

sql 复制代码
select * from product where id between 5 and 10 for update;

InnoDB 可能会锁住某些索引区间,防止其他事务在这个范围内插入数据。

作用

主要是防止幻读。


4.5 Next-Key Lock

Next-Key Lock = 行锁 + 间隙锁。

它锁住的是:

text 复制代码
左开右闭区间

例如:

text 复制代码
(5, 10]

InnoDB 在可重复读隔离级别下,常用 Next-Key Lock 防止幻读。


4.6 意向锁

意向锁是数据库内部使用的锁。

比如一个事务想给某几行加排他锁,InnoDB 会先在表级别加意向排他锁。

它的作用是提高表锁和行锁之间的冲突判断效率。

业务代码通常不用直接操作意向锁。


4.7 元数据锁 MDL

MySQL 中执行普通查询时,也会加元数据锁。

例如你在查询一张表时,另一个事务想修改表结构:

sql 复制代码
alter table product add column xxx int;

可能会被阻塞。

线上执行 DDL 卡住,很多时候就和 MDL 有关。


4.8 数据库乐观锁

这是业务开发中非常推荐的一种方式。

表中加 version 字段:

sql 复制代码
update account
set balance = balance - 100,
    version = version + 1
where id = 1
  and version = #{oldVersion}
  and balance >= 100;

Java 判断:

java 复制代码
int rows = accountMapper.deduct(id, oldVersion);

if (rows == 0) {
    throw new RuntimeException("更新失败,请重试");
}

优点

  • 不长时间占用数据库锁;
  • 性能较好;
  • 适合高并发更新。

缺点

  • 需要处理失败重试;
  • 冲突严重时性能会下降。

4.9 唯一索引也可以当"锁"

比如防止重复下单。

表:

sql 复制代码
create unique index uk_order_user_product
on orders(user_id, product_id);

插入订单:

sql 复制代码
insert into orders(user_id, product_id, order_no)
values(1, 100, 'xxx');

如果同一个用户对同一个商品重复下单,数据库唯一索引会直接拦截。

适合场景

  • 幂等控制;
  • 防重复提交;
  • 防重复消费消息。

很多时候,唯一索引比加锁更可靠。


5. 分布式锁

当应用部署了多个实例时,JVM 锁就不够了。

例如:

text 复制代码
order-service-1
order-service-2
order-service-3

三个实例都可能处理同一个商品库存。

此时需要分布式锁。

分布式锁的目标是:

在多个 JVM、多个机器、多个服务实例之间,同一时刻只有一个实例可以操作某个资源。


6. Redis 分布式锁

Redis 分布式锁是 Java 后端最常用的分布式锁方案之一。


6.1 基础写法

正确加锁方式:

redis 复制代码
SET lock:order:100 uniqueValue NX PX 30000

含义:

  • NX:key 不存在才设置成功;
  • PX 30000:过期时间 30 秒;
  • uniqueValue:锁的持有者标识,防止误删别人的锁。

释放锁时不能直接:

redis 复制代码
DEL lock:order:100

因为可能删掉别人的锁。

正确方式是 Lua 脚本:

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

6.2 Redis 锁必须注意的问题

第一,必须设置过期时间

否则服务宕机后,锁永远不释放。


第二,释放锁时必须校验 owner

防止 A 的锁过期后,B 拿到锁,结果 A 执行完后误删 B 的锁。


第三,锁过期时间要合理

如果业务执行 60 秒,锁 30 秒就过期了,会出现并发问题。


第四,最好支持自动续期

比如 Redisson 的 watchdog 机制。


第五,极端场景需要 fencing token

比如:

  1. A 拿到锁;
  2. A 发生长时间 STW 或网络卡顿;
  3. 锁过期;
  4. B 拿到锁并完成操作;
  5. A 恢复后继续操作共享资源。

这时即使使用 Redis 锁,也可能出问题。

解决方案是引入 fencing token,即单调递增令牌。

谁的 token 更新,谁才有资格操作下游资源。


6.3 Redisson

Java 中使用 Redis 分布式锁,推荐 Redisson。

依赖示例:

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>

使用方式:

java 复制代码
@Autowired
private RedissonClient redissonClient;

public void createOrder(Long productId) {
    RLock lock = redissonClient.getLock("lock:product:" + productId);

    boolean locked = false;

    try {
        locked = lock.tryLock(3, 30, TimeUnit.SECONDS);

        if (!locked) {
            throw new RuntimeException("系统繁忙,请稍后重试");
        }

        // 执行业务逻辑
        // 查库存、扣库存、创建订单

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取锁被中断", e);
    } finally {
        if (locked && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Redisson 常见锁类型

类型 说明
RLock 普通可重入分布式锁
RFairLock 公平分布式锁
RReadWriteLock 分布式读写锁
RMultiLock 多锁组合
RedLock 基于多个 Redis 节点的锁,存在争议
RSemaphore 分布式信号量
RPermitExpirableSemaphore 带过期时间的信号量
RCountDownLatch 分布式闭锁
RFencedLock 带 fencing token 思路的锁,新版本中更适合强一致保护场景

7. Zookeeper 分布式锁

Zookeeper 也常用于分布式锁。

它的核心机制是:

  • 临时节点;
  • 顺序节点;
  • Watcher 监听;
  • Session 失效自动删除节点。

大致流程:

  1. 客户端在某个目录下创建临时顺序节点;
  2. 谁的节点序号最小,谁获得锁;
  3. 其他客户端监听自己前一个节点;
  4. 前一个节点删除后,自己尝试获取锁。

常用客户端是 Apache Curator。

示例:

java 复制代码
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");

try {
    if (lock.acquire(3, TimeUnit.SECONDS)) {
        // 执行业务
    }
} finally {
    if (lock.isAcquiredInThisProcess()) {
        lock.release();
    }
}

特点

  • 一致性强;
  • 适合分布式协调;
  • 可实现公平锁;
  • 性能不如 Redis;
  • 运维复杂度比 Redis 高。

适用场景

  • 分布式锁;
  • Leader 选举;
  • 配置管理;
  • 分布式协调。

8. etcd 分布式锁

etcd 也可以实现分布式锁。

它基于:

  • Lease 租约;
  • Revision 版本号;
  • CAS;
  • Watch 机制。

etcd 在 Kubernetes 体系中用得很多。

特点

  • 强一致;
  • 适合服务协调;
  • 可以提供 fencing token 类似能力;
  • 运维要求较高。

9. 数据库实现分布式锁

如果系统规模不大,也可以用数据库实现分布式锁。


9.1 基于唯一索引

表:

sql 复制代码
create table distributed_lock (
    lock_name varchar(100) primary key,
    owner varchar(100),
    expire_time datetime
);

加锁:

sql 复制代码
insert into distributed_lock(lock_name, owner, expire_time)
values('lock:order:100', 'instance-1', now() + interval 30 second);

插入成功表示拿到锁。

释放锁:

sql 复制代码
delete from distributed_lock
where lock_name = 'lock:order:100'
  and owner = 'instance-1';

优点

  • 简单;
  • 不依赖 Redis/Zookeeper;
  • 和业务数据库一致性好。

缺点

  • 性能一般;
  • 容易给数据库增加压力;
  • 需要处理过期锁。

9.2 基于 select for update

sql 复制代码
select * from distributed_lock
where lock_name = 'lock:order:100'
for update;

这个方式依赖数据库事务。

适合一些低并发、强事务一致性的场景。


9.3 MySQL GET_LOCK

MySQL 还提供:

sql 复制代码
select get_lock('lock:order:100', 10);
select release_lock('lock:order:100');

但它和数据库连接绑定,连接池场景下要非常小心。

一般业务系统中不太推荐大规模使用。


10. Spring 中有没有现成注解?

这个问题要分场景看。


10.1 Spring 原生有没有通用的分布式锁注解?

结论:

Spring Framework 本身没有提供一个通用的 @DistributedLock 注解。

也就是说,Spring 官方没有类似下面这种开箱即用注解:

java 复制代码
@DistributedLock("lock:order:#{orderId}")
public void createOrder(Long orderId) {
}

如果你看到这种注解,通常是:

  • 公司内部封装的;
  • 基于 AOP 自己实现的;
  • 第三方 starter 提供的;
  • 基于 Redisson、Redis、Zookeeper、数据库封装的。

10.2 Spring 事务注解 @Transactional

虽然 @Transactional 不是锁注解,但它和数据库锁强相关。

例如:

java 复制代码
@Transactional
public void deductStock(Long productId) {
    Product product = productMapper.selectForUpdate(productId);
    // 扣库存
}

只有在事务内:

sql 复制代码
select ... for update

才会一直持有锁直到事务提交或回滚。


10.3 Spring Data JPA 的 @Lock

如果你使用 Spring Data JPA,可以使用 @Lock

java 复制代码
public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select p from Product p where p.id = :id")
    Optional<Product> findByIdForUpdate(@Param("id") Long id);
}

常见模式:

java 复制代码
LockModeType.PESSIMISTIC_WRITE
LockModeType.PESSIMISTIC_READ
LockModeType.OPTIMISTIC
LockModeType.OPTIMISTIC_FORCE_INCREMENT

悲观写锁

java 复制代码
@Lock(LockModeType.PESSIMISTIC_WRITE)

类似数据库的:

sql 复制代码
select ... for update

10.4 JPA 的 @Version

JPA 支持乐观锁注解:

java 复制代码
@Entity
public class Product {

    @Id
    private Long id;

    private Integer stock;

    @Version
    private Integer version;
}

更新时 JPA 会自动带上 version 判断。

如果版本冲突,会抛出类似:

java 复制代码
OptimisticLockException

10.5 @Cacheable(sync = true)

Spring Cache 中有一个参数:

java 复制代码
@Cacheable(value = "user", key = "#id", sync = true)
public User getUser(Long id) {
    return userMapper.selectById(id);
}

作用是:

同一个 key 缓存失效时,同一时间只允许一个线程加载数据。

它可以缓解缓存击穿。

但注意:

  • 通常只对单 JVM 内有效;
  • 是否跨 JVM 生效取决于具体缓存实现;
  • 它不是通用分布式锁。

10.6 ShedLock 的 @SchedulerLock

如果你的问题是:

多个服务实例部署时,@Scheduled 定时任务会重复执行,怎么办?

可以用 ShedLock。

依赖 Redis、JDBC、Mongo、Zookeeper 等存储锁。

示例:

java 复制代码
@Scheduled(cron = "0 0/5 * * * ?")
@SchedulerLock(
    name = "syncOrderTask",
    lockAtMostFor = "10m",
    lockAtLeastFor = "1m"
)
public void syncOrderTask() {
    // 定时任务逻辑
}

这个注解不是 Spring 原生的,是 ShedLock 提供的。


10.7 Spring Integration LockRegistry

Spring Integration 提供了 LockRegistry 抽象。

常见实现:

  • JdbcLockRegistry
  • RedisLockRegistry
  • ZookeeperLockRegistry

使用方式类似:

java 复制代码
@Autowired
private LockRegistry lockRegistry;

public void handle(String orderNo) {
    Lock lock = lockRegistry.obtain("lock:order:" + orderNo);

    if (lock.tryLock()) {
        try {
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
}

它不是注解式用法,但属于 Spring 生态中的锁组件。


10.8 Lombok 的 @Synchronized

Lombok 有一个注解:

java 复制代码
@Synchronized
public void method() {
}

它不是 Spring 的,是 Lombok 的。

本质类似:

java 复制代码
synchronized

适合 JVM 内同步,不是分布式锁。


11. 自定义 @DistributedLock 注解

很多公司会基于 Redisson + AOP 自己封装一个注解。

例如:

java 复制代码
@DistributedLock(
    key = "'lock:order:' + #orderId",
    waitTime = 3,
    leaseTime = 30
)
public void createOrder(Long orderId) {
    // 创建订单
}

大致实现思路:

  1. 定义注解;
  2. 使用 Spring AOP 拦截方法;
  3. 解析 SpEL 表达式生成锁 key;
  4. 使用 Redisson 获取锁;
  5. 执行业务方法;
  6. finally 中释放锁。

示例注解:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

    String key();

    long waitTime() default 3;

    long leaseTime() default 30;

    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

12. 常见业务场景如何选锁?

12.1 单机服务内共享变量

推荐:

  • synchronized
  • ReentrantLock
  • AtomicInteger
  • LongAdder

例如本地计数器,优先考虑:

java 复制代码
LongAdder

12.2 多读少写的本地缓存

推荐:

  • ReentrantReadWriteLock
  • StampedLock

12.3 扣库存

如果是单体应用:

  • 数据库乐观锁;
  • 数据库悲观锁。

如果是分布式系统:

  • 优先考虑数据库原子更新;
  • 或 Redis 分布式锁;
  • 或库存预扣模型;
  • 高并发秒杀场景可用 Redis + MQ + 异步落库。

常见安全 SQL:

sql 复制代码
update product
set stock = stock - 1
where id = #{id}
  and stock > 0;

这个语句本身就是原子操作。

很多时候不一定非要加分布式锁。


12.4 防重复下单

优先推荐:

  • 唯一索引;
  • 幂等表;
  • Redis SET NX
  • 分布式锁。

例如:

sql 复制代码
create unique index uk_user_product
on orders(user_id, product_id);

12.5 定时任务多节点只执行一次

推荐:

  • ShedLock;
  • Quartz 集群模式;
  • XXL-JOB;
  • ElasticJob;
  • Redisson 锁。

12.6 防缓存击穿

单 JVM:

java 复制代码
@Cacheable(sync = true)

分布式环境:

  • Redis 分布式锁;
  • 逻辑过期;
  • 热点 key 预热;
  • Caffeine 本地缓存;
  • 请求合并。

12.7 分布式强一致协调

推荐:

  • Zookeeper;
  • etcd;
  • Consul。

例如:

  • Leader 选举;
  • 分布式任务调度;
  • 集群协调。

13. 使用锁的常见坑

13.1 锁粒度太大

比如:

java 复制代码
synchronized (OrderService.class)

这会导致所有订单都串行执行。

更好的方式是按订单号、商品 ID、用户 ID 加锁。

例如:

java 复制代码
lock:product:100
lock:user:200
lock:order:ORDER_001

13.2 忘记释放锁

显式锁一定要:

java 复制代码
try {
    lock.lock();
} finally {
    lock.unlock();
}

Redis、Redisson、Zookeeper 锁也一样。


13.3 锁里执行太慢的操作

不要在锁里做太慢的事情,比如:

  • 远程 RPC;
  • 大批量查询;
  • 文件上传;
  • 复杂计算;
  • 调用第三方接口。

锁持有时间越长,并发性能越差。


13.4 Redis 锁没有过期时间

这是大忌。

一旦服务宕机,锁可能永远存在。


13.5 Redis 锁直接 DEL

不能直接:

redis 复制代码
DEL lock:key

必须校验 value,确认是自己的锁才能删。


13.6 分布式锁不等于绝对安全

分布式锁可能因为:

  • GC Stop The World;
  • 网络抖动;
  • Redis 主从切换;
  • 锁过期;
  • 客户端卡顿;

导致异常情况。

强一致场景要结合:

  • fencing token;
  • 数据库版本号;
  • 幂等机制;
  • 唯一索引;
  • 事务。

14. 简单选型总结

场景 推荐方案
单 JVM 简单同步 synchronized
单 JVM 复杂同步 ReentrantLock
单 JVM 读多写少 ReentrantReadWriteLockStampedLock
简单计数 AtomicLongLongAdder
控制并发数 Semaphore
数据库行级并发 select ... for update
数据库低冲突更新 乐观锁 version
防重复提交 唯一索引、幂等表
多实例互斥 Redis 分布式锁、Redisson
强一致分布式协调 Zookeeper、etcd
多节点定时任务 ShedLock、Quartz、XXL-JOB
缓存击穿 @Cacheable(sync = true)、Redis 锁、逻辑过期

15. 最后给一个实践建议

实际项目中,不要一上来就加分布式锁。

优先顺序通常是:

  1. 能用数据库原子更新,就用数据库原子更新;
  2. 能用唯一索引保证幂等,就用唯一索引;
  3. 单 JVM 问题,用 JVM 锁;
  4. 多实例互斥,考虑 Redis/Redisson;
  5. 强一致协调,考虑 Zookeeper/etcd;
  6. 定时任务互斥,优先用 ShedLock/Quartz/任务调度平台。

一句话总结:

锁只是并发控制手段之一,真正可靠的系统,通常是锁、事务、幂等、唯一约束、重试机制一起配合使用。

相关推荐
苍何1 小时前
清华团队做了个具身智能大脑,有点东西!
后端
fliter1 小时前
强类型的诅咒,还是 Rust 类型系统的生存指南
后端
亲亲小宝宝鸭1 小时前
前端性能监控:web-vitals
前端·性能优化·监控
用户8356290780511 小时前
Python 操作 PDF 附件:添加、查看与管理指南
后端·python
陆枫Larry2 小时前
可滚动页面背景填不满:`height: 100vh` vs `min-height: 100vh`
前端
Patrick_Wilson2 小时前
Squash Merge 的血缘陷阱:为什么删掉的代码又活了过来
前端·git·程序员
kyriewen3 小时前
今天的科技圈,全在抢英伟达的饭碗
前端·面试·ai编程
SouthernWind3 小时前
RAGFlow——结合本地知识库检索开发实战指南(包含聊天、检索本地的知识库文档和Agent模式)
前端