Start
很多前端同学在转向全栈,尤其是深入 Java 后端开发时,会发现一个明显的变化:前端更多关注的是页面交互、组件状态、接口调用和用户体验,而后端除了要处理业务逻辑,还必须面对一个更底层、更复杂的问题------并发。
在真实的后端系统里,同一时间可能有成百上千个请求同时进来。多个用户可能同时抢购同一件商品,多个服务实例可能同时执行同一个定时任务,多个线程可能同时修改同一份数据。如果没有合适的并发控制,就可能出现库存超卖、重复下单、数据覆盖、缓存击穿、任务重复执行等问题。
而"锁",就是 Java 后端解决并发问题时非常核心的一类工具。
从单机应用中的 synchronized、ReentrantLock,到数据库里的悲观锁、乐观锁、行锁,再到分布式系统中的 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 悲观锁
悲观锁的思想是:
我认为别人一定会来抢,所以我先加锁。
典型实现:
synchronizedReentrantLock- 数据库
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
比如:
- A 拿到锁;
- A 发生长时间 STW 或网络卡顿;
- 锁过期;
- B 拿到锁并完成操作;
- 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 失效自动删除节点。
大致流程:
- 客户端在某个目录下创建临时顺序节点;
- 谁的节点序号最小,谁获得锁;
- 其他客户端监听自己前一个节点;
- 前一个节点删除后,自己尝试获取锁。
常用客户端是 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 抽象。
常见实现:
JdbcLockRegistryRedisLockRegistryZookeeperLockRegistry
使用方式类似:
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) {
// 创建订单
}
大致实现思路:
- 定义注解;
- 使用 Spring AOP 拦截方法;
- 解析 SpEL 表达式生成锁 key;
- 使用 Redisson 获取锁;
- 执行业务方法;
- 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 单机服务内共享变量
推荐:
synchronizedReentrantLockAtomicIntegerLongAdder
例如本地计数器,优先考虑:
java
LongAdder
12.2 多读少写的本地缓存
推荐:
ReentrantReadWriteLockStampedLock
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 读多写少 | ReentrantReadWriteLock、StampedLock |
| 简单计数 | AtomicLong、LongAdder |
| 控制并发数 | Semaphore |
| 数据库行级并发 | select ... for update |
| 数据库低冲突更新 | 乐观锁 version |
| 防重复提交 | 唯一索引、幂等表 |
| 多实例互斥 | Redis 分布式锁、Redisson |
| 强一致分布式协调 | Zookeeper、etcd |
| 多节点定时任务 | ShedLock、Quartz、XXL-JOB |
| 缓存击穿 | @Cacheable(sync = true)、Redis 锁、逻辑过期 |
15. 最后给一个实践建议
实际项目中,不要一上来就加分布式锁。
优先顺序通常是:
- 能用数据库原子更新,就用数据库原子更新;
- 能用唯一索引保证幂等,就用唯一索引;
- 单 JVM 问题,用 JVM 锁;
- 多实例互斥,考虑 Redis/Redisson;
- 强一致协调,考虑 Zookeeper/etcd;
- 定时任务互斥,优先用 ShedLock/Quartz/任务调度平台。
一句话总结:
锁只是并发控制手段之一,真正可靠的系统,通常是锁、事务、幂等、唯一约束、重试机制一起配合使用。