核心概念总结

在深入细节之前,让我们先理解两种锁的核心思想:

  • 悲观锁(Pessimistic Locking):假设并发冲突一定会发生,在操作数据前先加锁,其他线程/事务必须等待。适合高并发冲突场景,通过加锁保证安全。
  • 乐观锁(Optimistic Locking):假设并发冲突很少发生,不加锁直接操作,提交时检测是否被别人改过。适合读多写少场景,通过版本检验或CAS保证一致性。

这两种策略的选择,本质上是在性能安全性之间的权衡。

悲观锁(Pessimistic Locking)

核心思想

悲观锁采用”先下手为强”的策略:假设并发冲突一定会发生,因此在操作数据前先加锁,其他线程/事务必须等待锁释放后才能操作

这种策略类似于现实生活中的”先到先得”:当你需要操作某个资源时,先把它锁起来,确保在你操作完成之前,其他人无法修改它。

工作原理

1
2
线程A: 获取锁 → 操作数据 → 提交事务 → 释放锁
线程B: 等待... → 等待... → 获取锁 → 操作数据 → 提交事务 → 释放锁

常见实现方式

1. 数据库层面的悲观锁

MySQL/PostgreSQL

1
2
3
4
5
6
7
8
-- 行级锁(排他锁)
SELECT * FROM account WHERE id = 1 FOR UPDATE;

-- 在事务中,这条记录会被锁定,直到事务提交
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT; -- 提交后锁才释放

SQL Server

1
2
-- 使用UPDLOCK提示
SELECT * FROM account WITH (UPDLOCK) WHERE id = 1;

2. Java语言层面的悲观锁

synchronized关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Account {
private int balance = 1000;

// 方法级锁
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}

// 代码块锁
public void transfer(Account target, int amount) {
synchronized (this) {
synchronized (target) {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
}
}
}
}
}

ReentrantLock(可重入锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.locks.ReentrantLock;

public class Account {
private int balance = 1000;
private final ReentrantLock lock = new ReentrantLock();

public void withdraw(int amount) {
lock.lock(); // 获取锁
try {
if (balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock(); // 确保释放锁
}
}
}

3. 其他语言的实现

Python

1
2
3
4
5
6
7
8
9
10
11
import threading

class Account:
def __init__(self):
self.balance = 1000
self.lock = threading.Lock()

def withdraw(self, amount):
with self.lock: # 自动获取和释放锁
if self.balance >= amount:
self.balance -= amount

详细示例:银行转账

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 事务1:用户A向用户B转账100元
BEGIN;
-- 锁定两个账户
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 账户A
SELECT * FROM account WHERE id = 2 FOR UPDATE; -- 账户B

-- 检查余额并转账
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 事务2:如果同时执行,必须等待事务1完成
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 等待事务1释放锁
-- ... 其他操作
COMMIT;

优缺点分析

✅ 优点

  1. 简单直接:实现逻辑清晰,容易理解
  2. 数据一致性强:通过锁机制保证数据不会被并发修改
  3. 适合高冲突场景:当并发冲突频繁时,悲观锁能有效避免数据不一致
  4. 避免重试开销:不需要像乐观锁那样可能反复重试

❌ 缺点

  1. 性能较差:线程阻塞导致吞吐量下降
  2. 容易产生死锁:多个锁的获取顺序不当可能导致死锁
  3. 扩展性不好:在高并发场景下,大量线程等待会严重影响性能
  4. 资源占用:锁会占用系统资源,降低系统整体性能

死锁问题

悲观锁容易产生死锁,需要特别注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 死锁示例:两个线程以不同顺序获取锁
// 线程1: 先锁A,再锁B
// 线程2: 先锁B,再锁A

// 解决方案1:统一锁的获取顺序
public void transfer(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;

synchronized (first) {
synchronized (second) {
// 转账逻辑
}
}
}

// 解决方案2:使用超时锁
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 操作
} finally {
lock.unlock();
}
}

适用场景

1. 银行转账系统

1
2
3
4
5
-- 必须保证账户余额的强一致性
BEGIN;
SELECT * FROM account WHERE id = ? FOR UPDATE;
-- 检查余额、转账等操作
COMMIT;

2. 库存强一致性要求

1
2
3
4
5
6
-- 电商系统中,库存必须准确
BEGIN;
SELECT stock FROM product WHERE id = ? FOR UPDATE;
-- 检查库存是否充足
UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0;
COMMIT;

3. 并发冲突非常频繁的场景

  • 抢票系统
  • 秒杀系统(如果冲突率极高)
  • 账户余额操作

4. 数据完整性要求极高的场景

  • 金融交易
  • 医疗记录
  • 法律文档

乐观锁(Optimistic Locking)

核心思想

乐观锁采用”先操作,后检查”的策略:假设并发冲突很少发生,因此不加锁直接操作数据,在提交时检测数据是否被别人修改过

这种策略类似于”先到先得”的另一种理解:相信大多数情况下不会有人同时修改,先进行操作,如果发现冲突再处理。

工作原理

1
2
线程A: 读取数据(version=1) → 修改数据 → 提交时检查version → version仍为1 → 提交成功
线程B: 读取数据(version=1) → 修改数据 → 提交时检查version → version已变为2 → 提交失败 → 重试

常见实现方式

1. 版本号机制(Version Number)

这是最常见的乐观锁实现方式,通过在数据表中增加一个版本号字段来实现。

表结构设计

1
2
3
4
5
CREATE TABLE account (
id INT PRIMARY KEY,
balance DECIMAL(10, 2),
version INT DEFAULT 0 -- 版本号字段
);

更新操作

1
2
3
4
5
6
7
8
9
10
11
-- 第一步:读取数据(包含版本号)
SELECT id, balance, version FROM account WHERE id = 1;
-- 假设读取到:balance = 1000, version = 3

-- 第二步:更新时检查版本号
UPDATE account
SET balance = balance - 100,
version = version + 1
WHERE id = 1 AND version = 3; -- 关键:WHERE条件包含版本号检查

-- 如果受影响行数为0,说明版本号不匹配,数据已被修改

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 事务1
BEGIN;
SELECT balance, version FROM account WHERE id = 1;
-- 读取到 balance=1000, version=1

-- 事务2(并发执行)
BEGIN;
SELECT balance, version FROM account WHERE id = 1;
-- 也读取到 balance=1000, version=1

-- 事务1先提交
UPDATE account
SET balance = 900, version = 2
WHERE id = 1 AND version = 1; -- 成功,受影响1行
COMMIT;

-- 事务2后提交
UPDATE account
SET balance = 900, version = 2
WHERE id = 1 AND version = 1; -- 失败,受影响0行(version已经是2了)
-- 需要回滚并重试
ROLLBACK;

2. CAS机制(Compare And Swap)

CAS是CPU级别的原子操作,Java的Atomic类就是基于CAS实现的。

Java中的CAS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
int current;
int next;
do {
current = count.get(); // 读取当前值
next = current + 1; // 计算新值
// CAS操作:如果当前值仍为current,则更新为next
} while (!count.compareAndSet(current, next));
// 如果CAS失败(说明值已被修改),循环重试
}

public int get() {
return count.get();
}
}

CAS的工作原理

1
2
3
4
5
6
7
8
// CAS操作的伪代码
public boolean compareAndSet(int expected, int update) {
if (this.value == expected) { // 比较当前值是否等于期望值
this.value = update; // 如果相等,更新为新值
return true;
}
return false; // 不相等,说明已被其他线程修改
}

更多Atomic类的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AtomicInteger
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.compareAndSet(0, 1); // 如果当前值是0,则设置为1

// AtomicReference(用于对象)
AtomicReference<String> atomicRef = new AtomicReference<>("initial");
atomicRef.compareAndSet("initial", "updated");

// AtomicStampedReference(带版本戳,解决ABA问题)
AtomicStampedReference<String> atomicStamped =
new AtomicStampedReference<>("value", 1);
int[] stampHolder = new int[1];
String current = atomicStamped.get(stampHolder);
atomicStamped.compareAndSet(current, "new", stampHolder[0], stampHolder[0] + 1);

3. 时间戳机制

使用时间戳代替版本号(较少使用,因为时间戳可能不准确):

1
2
3
4
5
6
7
8
9
CREATE TABLE account (
id INT PRIMARY KEY,
balance DECIMAL(10, 2),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

UPDATE account
SET balance = balance - 100
WHERE id = 1 AND update_time = '2026-01-12 10:00:00';

完整示例:版本号实现

Java + MyBatis实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Entity类
public class Account {
private Long id;
private BigDecimal balance;
private Integer version; // 版本号

// getters and setters
}

// Mapper接口
public interface AccountMapper {
Account selectById(Long id);
int updateWithVersion(Account account);
}

// Mapper XML
<!-- 更新时检查版本号 -->
<update id="updateWithVersion">
UPDATE account
SET balance = #{balance},
version = version + 1
WHERE id = #{id} AND version = #{version}
</update>

// Service层实现
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;

public boolean transfer(Long fromId, Long toId, BigDecimal amount) {
int maxRetries = 3;
int retries = 0;

while (retries < maxRetries) {
try {
// 读取账户信息(包含版本号)
Account from = accountMapper.selectById(fromId);
Account to = accountMapper.selectById(toId);

// 检查余额
if (from.getBalance().compareTo(amount) < 0) {
return false;
}

// 更新账户
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));

// 乐观锁更新
int rows1 = accountMapper.updateWithVersion(from);
int rows2 = accountMapper.updateWithVersion(to);

// 如果更新成功(受影响行数>0),说明版本号匹配
if (rows1 > 0 && rows2 > 0) {
return true;
}

// 更新失败,说明版本号不匹配,重试
retries++;
Thread.sleep(10); // 短暂等待后重试

} catch (Exception e) {
retries++;
if (retries >= maxRetries) {
throw new RuntimeException("转账失败,重试次数超限", e);
}
}
}

return false;
}
}

ABA问题及解决方案

ABA问题:值从A变成B,再变回A,CAS会认为值没有变化。

解决方案1:版本号/时间戳

1
2
3
4
5
6
7
// 使用AtomicStampedReference
AtomicStampedReference<Integer> atomic = new AtomicStampedReference<>(100, 1);
int[] stamp = new int[1];
int value = atomic.get(stamp); // value=100, stamp[0]=1

// 即使值变回100,版本号也不同
atomic.compareAndSet(100, 200, 1, 2); // 检查值和版本号

解决方案2:使用版本号字段

1
2
3
4
-- 数据库中的版本号机制天然解决了ABA问题
UPDATE account SET balance = 200, version = version + 1
WHERE id = 1 AND version = 1;
-- 即使balance变回原来的值,version也会递增

优缺点分析

✅ 优点

  1. 不阻塞线程:读取和修改操作不需要加锁,提高了并发性能
  2. 性能好:在低冲突场景下,性能远优于悲观锁
  3. 并发能力强:多个线程可以同时读取,提高了系统吞吐量
  4. 避免死锁:不需要获取锁,自然避免了死锁问题
  5. 适合分布式系统:在分布式环境下更容易实现

❌ 缺点

  1. 可能反复重试:如果冲突频繁,可能需要多次重试才能成功
  2. 实现复杂:需要处理重试逻辑、ABA问题等
  3. 冲突高时反而更慢:在高冲突场景下,反复重试的开销可能超过悲观锁
  4. 数据可能被丢弃:如果重试次数达到上限仍失败,操作可能无法完成

适用场景

1. 读多写少的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
// 文章阅读量统计
// 读操作远多于写操作,适合乐观锁
public void incrementViewCount(Long articleId) {
int maxRetries = 5;
for (int i = 0; i < maxRetries; i++) {
Article article = articleMapper.selectById(articleId);
article.setViewCount(article.getViewCount() + 1);
if (articleMapper.updateWithVersion(article) > 0) {
return; // 成功
}
// 失败则重试
}
}

2. 秒杀库存(配合重试机制)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 秒杀场景:读多写少,但写操作冲突可能较高
public boolean seckill(Long productId, Integer quantity) {
int maxRetries = 10;
for (int i = 0; i < maxRetries; i++) {
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
return false; // 库存不足
}

product.setStock(product.getStock() - quantity);
if (productMapper.updateWithVersion(product) > 0) {
return true; // 成功
}

// 冲突,短暂等待后重试
try {
Thread.sleep(10 + i * 5); // 递增等待时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false; // 重试次数超限
}

3. 分布式系统

1
2
3
4
5
6
7
8
9
// 分布式环境下,悲观锁需要分布式锁(如Redis),开销大
// 乐观锁只需要版本号,更适合分布式场景
@Service
public class DistributedAccountService {
public boolean updateBalance(String accountId, BigDecimal amount) {
// 乐观锁实现,无需分布式锁
// 通过版本号在数据库层面保证一致性
}
}

4. 缓存更新

1
2
3
4
5
6
7
8
9
10
// 缓存更新场景
public void updateCache(String key, Object value) {
CacheEntry entry = cache.get(key);
if (entry != null) {
// 使用CAS更新缓存
while (!cache.compareAndSet(key, entry, new CacheEntry(value, entry.version + 1))) {
entry = cache.get(key); // 重新读取
}
}
}

乐观锁 vs 悲观锁:全面对比

对比表格

对比项 乐观锁 悲观锁
核心思想 假设冲突少,先操作后检查 假设冲突多,先加锁后操作
是否加锁 不加锁 加锁
冲突处理 提交时校验,失败则重试 操作前阻塞,等待锁释放
性能 高(低冲突场景) 低(线程阻塞)
并发能力 强(多读并发) 弱(串行化执行)
实现复杂度 高(需处理重试、ABA问题) 低(直接加锁)
数据一致性 最终一致(可能重试) 强一致(立即保证)
死锁风险
适用场景 读多写少、冲突率低 写多冲突高、强一致性要求
资源消耗 低(无锁等待) 高(线程阻塞)
扩展性 好(适合分布式) 差(需要分布式锁)

性能对比分析

低冲突场景(冲突率 < 10%)

1
2
3
4
乐观锁:读取 → 修改 → 检查 → 提交(通常一次成功)
悲观锁:获取锁 → 等待 → 修改 → 提交 → 释放锁

结果:乐观锁性能明显优于悲观锁

高冲突场景(冲突率 > 50%)

1
2
3
4
乐观锁:读取 → 修改 → 检查失败 → 重试 → 检查失败 → 重试...(多次重试)
悲观锁:获取锁 → 等待 → 修改 → 提交 → 释放锁

结果:悲观锁性能可能优于乐观锁(避免重试开销)

选择决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
需要并发控制?

├─ 冲突率如何?
│ │
│ ├─ 冲突率低(< 20%)→ 乐观锁
│ │ │
│ │ └─ 读多写少? → 乐观锁(推荐)
│ │
│ └─ 冲突率高(> 50%)→ 悲观锁
│ │
│ └─ 强一致性要求? → 悲观锁(推荐)

├─ 性能要求?
│ │
│ ├─ 高吞吐量 → 乐观锁
│ │
│ └─ 低延迟、强一致 → 悲观锁

└─ 系统架构?

├─ 分布式系统 → 乐观锁(更容易实现)

└─ 单机系统 → 根据冲突率选择

混合使用策略

在实际项目中,可以根据不同场景混合使用两种锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class OrderService {

// 读多写少:使用乐观锁
public void updateOrderStatus(Long orderId, String status) {
// 乐观锁实现
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
Order order = orderMapper.selectById(orderId);
order.setStatus(status);
if (orderMapper.updateWithVersion(order) > 0) {
return;
}
}
throw new OptimisticLockException("更新失败,请重试");
}

// 写多冲突高:使用悲观锁
@Transactional
public void deductInventory(Long productId, Integer quantity) {
// 悲观锁实现
Product product = productMapper.selectByIdForUpdate(productId);
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productMapper.update(product);
} else {
throw new InsufficientStockException("库存不足");
}
}
}

实际应用案例

案例1:电商库存管理

场景分析

  • 商品浏览(读操作)远多于购买(写操作)
  • 热门商品购买时冲突率较高
  • 需要保证库存准确性

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class InventoryService {

// 普通商品:读多写少,使用乐观锁
public boolean purchaseNormalProduct(Long productId, Integer quantity) {
int maxRetries = 5;
for (int i = 0; i < maxRetries; i++) {
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
return false;
}
product.setStock(product.getStock() - quantity);
if (productMapper.updateWithVersion(product) > 0) {
return true;
}
// 重试
}
return false;
}

// 秒杀商品:冲突率极高,使用悲观锁
@Transactional
public boolean purchaseSeckillProduct(Long productId, Integer quantity) {
Product product = productMapper.selectByIdForUpdate(productId);
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productMapper.update(product);
return true;
}
return false;
}
}

案例2:账户余额管理

场景分析

  • 金融系统,强一致性要求
  • 转账操作冲突率中等
  • 不允许数据不一致

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class AccountService {

// 金融转账:使用悲观锁保证强一致性
@Transactional
public boolean transfer(Long fromId, Long toId, BigDecimal amount) {
// 按ID顺序获取锁,避免死锁
Account from = fromId < toId ?
accountMapper.selectByIdForUpdate(fromId) :
accountMapper.selectByIdForUpdate(fromId);
Account to = fromId < toId ?
accountMapper.selectByIdForUpdate(toId) :
accountMapper.selectByIdForUpdate(toId);

if (from.getBalance().compareTo(amount) >= 0) {
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountMapper.update(from);
accountMapper.update(to);
return true;
}
return false;
}
}

案例3:文章点赞系统

场景分析

  • 读操作(查看文章)远多于写操作(点赞)
  • 点赞冲突率低
  • 可以接受偶尔的点赞失败

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class ArticleService {

// 点赞:使用乐观锁
public boolean likeArticle(Long articleId, Long userId) {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
Article article = articleMapper.selectById(articleId);
// 检查是否已点赞
if (likeMapper.exists(articleId, userId)) {
return false;
}
// 更新点赞数
article.setLikeCount(article.getLikeCount() + 1);
if (articleMapper.updateWithVersion(article) > 0) {
likeMapper.insert(articleId, userId);
return true;
}
}
return false;
}
}

最佳实践

1. 选择合适的锁策略

  • 读多写少 → 乐观锁
  • 写多冲突高 → 悲观锁
  • 强一致性要求 → 悲观锁
  • 高并发、低冲突 → 乐观锁

2. 乐观锁实现建议

  • 设置合理的重试次数:避免无限重试
  • 使用递增等待时间:减少冲突
  • 处理ABA问题:使用版本号或时间戳
  • 记录失败日志:监控冲突率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean updateWithRetry(Long id, Function<Entity, Entity> updater) {
int maxRetries = 5;
for (int i = 0; i < maxRetries; i++) {
try {
Entity entity = mapper.selectById(id);
Entity updated = updater.apply(entity);
if (mapper.updateWithVersion(updated) > 0) {
return true;
}
// 递增等待时间
Thread.sleep(10 * (i + 1));
} catch (Exception e) {
logger.warn("更新失败,重试中...", e);
}
}
return false;
}

3. 悲观锁实现建议

  • 统一锁的获取顺序:避免死锁
  • 使用超时锁:避免长时间等待
  • 尽快释放锁:减少锁持有时间
  • 避免嵌套锁:降低死锁风险
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 好的实践:统一锁顺序
public void transfer(Account a, Account b, BigDecimal amount) {
Account first = a.getId() < b.getId() ? a : b;
Account second = a.getId() < b.getId() ? b : a;

synchronized (first) {
synchronized (second) {
// 转账逻辑
}
}
}

// 使用超时锁
ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 操作
} finally {
lock.unlock();
}
} else {
throw new TimeoutException("获取锁超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

4. 监控和优化

  • 监控锁冲突率:决定是否需要调整策略
  • 监控重试次数:优化乐观锁的重试逻辑
  • 监控死锁:及时发现和解决死锁问题
  • 性能测试:在不同场景下测试两种锁的性能