Java后端锁机制详解
本文系统讲解Java后端开发中的锁机制,从JVM层的synchronized和ReentrantLock,到数据库层的悲观锁和乐观锁,再到分布式环境下的分布式锁和看门狗机制,配合大量实战代码,帮助你全面掌握并发控制的核心技术。
目录
1. 什么是锁
锁是用于控制多个并发访问同一资源的机制。在多线程、多事务、多节点的环境中,锁保证数据的一致性和完整性。
1.1 为什么需要锁
没有锁的世界: 线程A 读取库存 = 10 ──────────────────→ 写入库存 = 9(扣减1) 线程B 读取库存 = 10 ──────────────────→ 写入库存 = 9(扣减1) 结果:卖了2件,库存只减了1 ❌(超卖) 有锁的世界: 线程A 获取锁 → 读取库存=10 → 扣减1 → 写入库存=9 → 释放锁 线程B 等待锁 ───────────────────→ 获取锁 → 读取库存=9 → 扣减1 → 写入库存=8 → 释放锁 结果:正确扣减 ✅
1.2 锁的本质
锁的本质是"排队":把并发操作变成串行操作 - 没有锁:所有人同时操作 → 数据错乱 - 有锁:一个人操作,其他人等待 → 数据正确 代价:牺牲了并发性能,换取数据一致性 目标:在保证数据正确的前提下,尽可能提高并发度
2. 锁的分类总览
锁可以从多个维度进行分类,理解分类有助于在不同场景下选择合适的锁。
2.1 按粒度分类
┌─────────────────────────────────────────────────────────────┐ │ 锁的粒度 │ ├──────────────┬──────────────────────────────────────────────┤ │ 行级锁 │ 锁定单行数据,粒度最细,并发度最高 │ │ (Row Lock) │ InnoDB默认支持行锁 │ ├──────────────┼──────────────────────────────────────────────┤ │ 表级锁 │ 锁定整张表,粒度最粗,并发度最低 │ │ (Table Lock)│ MyISAM只支持表锁 │ ├──────────────┼──────────────────────────────────────────────┤ │ 页级锁 │ 锁定一页数据(BDB引擎),介于行锁和表锁之间 │ │ (Page Lock) │ MySQL的BDB引擎使用 │ └──────────────┴──────────────────────────────────────────────┘
2.2 按模式分类
┌─────────────────────────────────────────────────────────────┐ │ 锁的模式 │ ├──────────────┬──────────────────────────────────────────────┤ │ 共享锁 │ 又叫读锁(S锁),多个事务可以同时持有 │ │ (Shared Lock)│ 用于读操作,读读不互斥 │ ├──────────────┼──────────────────────────────────────────────┤ │ 排他锁 │ 又叫写锁(X锁),同一时刻只能一个事务持有 │ │ (Exclusive) │ 用于写操作,读写互斥、写写互斥 │ ├──────────────┼──────────────────────────────────────────────┤ │ 意向锁 │ 表级锁,表示事务打算对表中的行加锁 │ │ (Intention) │ 意向共享锁(IS) / 意向排他锁(IX) │ └──────────────┴──────────────────────────────────────────────┘ 互斥关系矩阵: ┌─────┬─────┬─────┐ │ │ S │ X │ ├─────┼─────┼─────┤ │ S │ 兼容 │ 互斥 │ ├─────┼─────┼─────┤ │ X │ 互斥 │ 互斥 │ └─────┴─────┴─────┘
2.3 按思想/策略分类
┌─────────────────────────────────────────────────────────────┐ │ 锁的策略 │ ├──────────────┬──────────────────────────────────────────────┤ │ 悲观锁 │ 假设一定会发生冲突 │ │ (Pessimistic)│ 先加锁,再操作 │ │ │ 数据库:SELECT ... FOR UPDATE │ ├──────────────┼──────────────────────────────────────────────┤ │ 乐观锁 │ 假设不会发生冲突 │ │ (Optimistic)│ 先操作,提交时检查是否冲突 │ │ │ 实现:版本号 / CAS │ └──────────────┴──────────────────────────────────────────────┘
2.4 按应用层次分类
┌─────────────────────────────────────────────────────────────┐ │ 锁的层次 │ ├──────────────┬──────────────────────────────────────────────┤ │ JVM进程内锁 │ synchronized / ReentrantLock │ │ (单机多线程) │ 同一个JVM中的多个线程 │ ├──────────────┼──────────────────────────────────────────────┤ │ 数据库层锁 │ 悲观锁 / 乐观锁 / 行锁 / 表锁 │ │ (单机多事务) │ 同一个数据库的多个事务 │ ├──────────────┼──────────────────────────────────────────────┤ │ 分布式锁 │ Redis锁 / Zookeeper锁 │ │ (多机多节点) │ 多台服务器的多个进程 │ └──────────────┴──────────────────────────────────────────────┘ 它们解决的是同一类问题(并发控制),但适用的规模不同: JVM锁 → 管不了其他机器 数据库锁 → 管不了其他数据库 分布式锁 → 能管所有节点
3. JVM层面的锁
JVM层面的锁用于解决单机多线程环境下的并发问题。
3.1 synchronized
synchronized是Java内置的关键字,基于对象的监视器(Monitor)实现。
public class SynchronizedDemo {
private int count = 0;
/**
* 1. 修饰实例方法:锁的是当前对象实例(this)
*/
public synchronized void increment() {
count++;
}
/**
* 2. 修饰静态方法:锁的是当前类的Class对象
*/
public static synchronized void staticIncrement() {
// 锁的是 SynchronizedDemo.class
}
/**
* 3. 修饰代码块:锁的是指定的对象
*/
public void blockIncrement() {
// 锁的是this对象
synchronized (this) {
count++;
}
// 也可以锁其他对象
Object lock = new Object();
synchronized (lock) {
// 临界区代码
}
}
public int getCount() {
return count;
}
}
synchronized的锁升级
JDK 1.6之后,synchronized引入了锁升级机制,大大提高了性能:
锁状态升级过程: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 ↑ ↑ ↑ 只有一个线程 多个线程交替 多个线程同时竞争 访问(最乐观)访问(较乐观)访问(最悲观) 偏向锁:在对象头中记录线程ID,下次同一线程进入时无需CAS操作 轻量级锁:通过CAS自旋获取锁,不阻塞线程 重量级锁:通过操作系统互斥量实现,线程会被阻塞挂起
synchronized的底层原理
字节码层面: synchronized代码块 → monitorenter / monitorexit 指令 synchronized方法 → 方法标志 ACC_SYNCHRONIZED 对象头(Mark Word)中存储锁信息: ┌──────────────────────────────────────────────────┐ │ Mark Word (64bit) │ ├──────────────────────────────────────────────────┤ │ 无锁状态:hashcode | age | biased_lock=0 | 01 │ │ 偏向锁:thread_id | age | biased_lock=1 | 01 │ │ 轻量级锁:指向栈中锁记录的指针 | 00 │ │ 重量级锁:指向Monitor对象的指针 | 10 │ └──────────────────────────────────────────────────┘
3.2 ReentrantLock
ReentrantLock是Java.util.concurrent包中的锁,基于AQS(AbstractQueuedSynchronizer)实现,比synchronized更灵活。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
/**
* 基本用法
*/
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
/**
* 尝试获取锁(非阻塞)
*/
public boolean tryIncrement() {
// 尝试获取锁,获取不到立即返回false
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 没获取到锁
}
/**
* 带超时的尝试获取锁
*/
public boolean tryIncrementWithTimeout() throws InterruptedException {
// 尝试获取锁,最多等待3秒
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 超时未获取到锁
}
public int getCount() {
return count;
}
}
ReentrantLock的高级特性
import java.util.concurrent.locks.*;
import java.util.concurrent.TimeUnit;
public class AdvancedReentrantLockDemo {
// ==================== 1. 公平锁与非公平锁 ====================
// 非公平锁(默认):新来的线程可以插队,吞吐量高
private final ReentrantLock unfairLock = new ReentrantLock(false);
// 公平锁:严格按照请求顺序获取锁,吞吐量低
private final ReentrantLock fairLock = new ReentrantLock(true);
// ==================== 2. 可中断锁 ====================
private final ReentrantLock interruptibleLock = new ReentrantLock();
public void interruptibleLockDemo() throws InterruptedException {
// 等待锁的过程中可以被中断
interruptibleLock.lockInterruptibly();
try {
// 业务逻辑
} finally {
interruptibleLock.unlock();
}
}
// 线程B中断等待中的线程A
// threadA.interrupt(); // 线程A的lockInterruptibly()会抛出InterruptedException
// ==================== 3. Condition条件变量 ====================
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final java.util.LinkedList<String> queue = new java.util.LinkedList<>();
private static final int MAX_SIZE = 10;
// 生产者
public void put(String item) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= MAX_SIZE) {
notFull.await(); // 队列满了,等待"未满"条件
}
queue.addLast(item);
notEmpty.signal(); // 通知消费者:队列非空了
} finally {
lock.unlock();
}
}
// 消费者
public String take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空了,等待"非空"条件
}
String item = queue.removeFirst();
notFull.signal(); // 通知生产者:队列未满了
return item;
} finally {
lock.unlock();
}
}
}
3.3 synchronized vs ReentrantLock
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置关键字,基于Monitor | JDK类库,基于AQS |
| 使用方式 | 自动获取/释放 | 手动lock()/unlock() |
| 是否可中断 | 不可中断 | 可中断(lockInterruptibly) |
| 是否公平 | 非公平 | 可选公平/非公平 |
| 是否可超时 | 不支持 | 支持(tryLock超时) |
| 条件变量 | 只有一个(wait/notify) | 多个Condition |
| 锁升级 | 支持(偏向→轻量→重量) | 不支持 |
| 性能 | JDK1.6后与ReentrantLock接近 | 略好于旧版synchronized |
选择建议:
简单同步场景 → synchronized(代码简洁,不会忘记释放锁) 需要高级特性 → ReentrantLock(可中断、超时、公平、多条件)
3.4 ReadWriteLock(读写锁)
读写锁将读操作和写操作分离,读读不互斥,读写互斥,写写互斥。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.HashMap;
import java.util.Map;
public class ReadWriteLockDemo {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, String> cache = new HashMap<>();
/**
* 读操作:使用读锁,多个线程可以同时读
*/
public String get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
/**
* 写操作:使用写锁,同一时刻只能一个线程写
*/
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
/**
* 读写锁的性能优势:
*
* 场景:100个线程读,5个线程写
*
* 普通锁(synchronized/ReentrantLock):
* 105个线程全部串行执行
*
* 读写锁(ReadWriteLock):
* 100个读线程可以并发执行
* 5个写线程串行执行
* 读写之间互斥
* 性能大幅提升
*/
}
3.5 StampedLock(JDK8新增)
StampedLock是ReadWriteLock的增强版,支持乐观读。
import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo {
private final StampedLock sl = new StampedLock();
private double x, y; // 坐标
/**
* 写操作
*/
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
/**
* 乐观读(不加锁,性能最高)
* 适合读多写少的场景
*/
public double distanceFromOrigin() {
// 1. 乐观读:获取一个版本号(stamp)
long stamp = sl.tryOptimisticRead();
// 2. 读取数据(此时没有加锁)
double currentX = x;
double currentY = y;
// 3. 验证版本号:如果期间没有写操作,验证通过
if (sl.validate(stamp)) {
// 验证通过,直接返回结果(没有加锁,性能最高)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 4. 验证失败(期间有写操作),升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
return Math.sqrt(currentX * currentX + currentY * currentY);
} finally {
sl.unlockRead(stamp);
}
}
}
4. 数据库层面的锁
数据库层面的锁用于解决多事务并发访问同一数据的问题。
4.1 InnoDB的锁类型
InnoDB存储引擎支持的锁: 按粒度分: ├── 表级锁 │ ├── 表锁(LOCK TABLES ... READ/WRITE) │ ├── 意向锁(IS / IX) │ └── AUTO-INC锁(自增列插入) │ └── 行级锁 ├── 记录锁(Record Lock):锁定单行记录 ├── 间隙锁(Gap Lock):锁定记录之间的间隙,防止幻读 └── 临键锁(Next-Key Lock):记录锁 + 间隙锁,左开右闭区间
4.2 记录锁(Record Lock)
锁定索引中的一条记录。
-- 锁定id=1的记录 SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 其他事务不能修改id=1的记录 -- 其他事务可以插入id=2的记录
4.3 间隙锁(Gap Lock)
锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录(防止幻读)。
-- 假设表中有id=1, 5, 10的记录 -- 以下查询会锁定(1,5)和(5,10)的间隙 SELECT * FROM user WHERE id BETWEEN 2 AND 9 FOR UPDATE; -- 其他事务不能在间隙中插入id=2,3,4,6,7,8,9的记录 -- 其他事务可以插入id=11的记录(不在间隙范围内)
4.4 临键锁(Next-Key Lock)
记录锁 + 间隙锁,锁定记录本身以及记录之前的间隙。InnoDB在REPEATABLE READ级别下默认使用临键锁。
-- 假设表中有id=1, 5, 10的记录 -- 以下查询会锁定 (负无穷,1], (1,5], (5,10] 的区间 SELECT * FROM user WHERE id = 5 FOR UPDATE; -- 锁定范围:(1, 5] 即 id=5的记录 + (1,5)的间隙
5. 悲观锁
悲观锁的核心思想:假设一定会发生并发冲突,所以在操作数据之前先加锁,确保同一时刻只有一个线程能操作数据。
5.1 数据库悲观锁(SELECT ... FOR UPDATE)
/**
* 悲观锁示例:扣减库存
* 使用 SELECT ... FOR UPDATE 锁定行记录
*/
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
/**
* 悲观锁扣减库存
*
* 执行流程:
* 1. SELECT ... FOR UPDATE 锁定该行
* 2. 其他事务对该行的读写都会被阻塞
* 3. 当前事务提交/回滚后,锁自动释放
*/
@Transactional(rollbackFor = Exception.class)
public boolean deductStock(Long productId, Integer quantity) {
// 加排他锁,其他事务读这行会被阻塞
Stock stock = stockMapper.selectForUpdate(productId);
if (stock == null) {
throw new BusinessException("商品不存在");
}
if (stock.getQuantity() < quantity) {
throw new BusinessException("库存不足");
}
// 扣减库存
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
return true;
// 事务提交时,行锁自动释放
}
}
对应的Mapper:
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
/**
* SELECT ... FOR UPDATE 加排他锁
*/
@Select("SELECT * FROM stock WHERE product_id = #{productId} FOR UPDATE")
Stock selectForUpdate(@Param("productId") Long productId);
/**
* SELECT ... LOCK IN SHARE MODE 加共享锁
* 允许其他事务也加共享锁读取,但不允许写
*/
@Select("SELECT * FROM stock WHERE product_id = #{productId} LOCK IN SHARE MODE")
Stock selectForShare(@Param("productId") Long productId);
}
5.2 FOR UPDATE 的使用注意事项
@Service
public class PessimisticLockService {
/**
* 注意事项1:FOR UPDATE 必须在事务中使用
* 没有事务,锁会立即释放,等于没加锁
*/
@Transactional(rollbackFor = Exception.class)
public void correctUsage() {
Stock stock = stockMapper.selectForUpdate(1L);
// 锁会持续到事务结束
}
// ❌ 错误:没有事务,FOR UPDATE的锁会立即释放
public void wrongUsage() {
Stock stock = stockMapper.selectForUpdate(1L);
// 查询完锁就释放了,后续操作没有保护
}
/**
* 注意事项2:FOR UPDATE 必须命中索引
* 如果没命中索引,会退化为表锁,严重影响并发性能
*/
// ✅ 正确:product_id是索引列
// SELECT * FROM stock WHERE product_id = 1 FOR UPDATE → 行锁
@Select("SELECT * FROM stock WHERE product_id = #{id} FOR UPDATE")
Stock selectForUpdateById(@Param("id") Long id);
// ❌ 危险:name不是索引列
// SELECT * FROM stock WHERE name = 'test' FOR UPDATE → 表锁!
@Select("SELECT * FROM stock WHERE name = #{name} FOR UPDATE")
Stock selectForUpdateByName(@Param("name") String name);
/**
* 注意事项3:NOWAIT 和 SKIP LOCKED
*/
// NOWAIT:获取不到锁立即返回错误,不等待
@Select("SELECT * FROM stock WHERE product_id = #{id} FOR UPDATE NOWAIT")
Stock selectForUpdateNowait(@Param("id") Long id);
// SKIP LOCKED:跳过已被锁定的行
@Select("SELECT * FROM stock WHERE status = 0 FOR UPDATE SKIP LOCKED")
List<Stock> selectUnprocessed();
}
5.3 悲观锁的死锁问题
/**
* 死锁示例:两个事务互相等待对方释放锁
*
* 事务A: 事务B:
* 1. 锁定账户1 1. 锁定账户2
* 2. 尝试锁定账户2 → 等待B释放 2. 尝试锁定账户1 → 等待A释放
* → 死锁!
*/
@Service
public class TransferService {
/**
* 避免死锁的方法:按固定顺序获取锁
* 始终按ID从小到大的顺序锁定
*/
@Transactional(rollbackFor = Exception.class)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 按ID大小排序,始终先锁ID小的
Long firstId = Math.min(fromId, toId);
Long secondId = Math.max(fromId, toId);
Account first = accountMapper.selectForUpdate(firstId);
Account second = accountMapper.selectForUpdate(secondId);
// 根据fromId和toId确定谁扣款谁收款
Account from = fromId.equals(firstId) ? first : second;
Account to = fromId.equals(firstId) ? second : first;
if (from.getBalance().compareTo(amount) < 0) {
throw new BusinessException("余额不足");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountMapper.updateById(from);
accountMapper.updateById(to);
}
}
5.4 悲观锁的优缺点
优点: ✓ 简单直观,容易理解 ✓ 数据安全性高,不会出现脏数据 ✓ 适合写多读少、冲突频繁的场景 ✓ 实现简单,不容易出错 缺点: ✗ 并发性能差,大量请求串行等待 ✗ 可能产生死锁 ✗ 锁的持有时间长,影响系统吞吐量 ✗ 不适合高并发读的场景 ✗ 如果SQL没走索引,会退化为表锁
6. 乐观锁
乐观锁的核心思想:假设不会发生冲突,操作时不加锁,但在提交更新时检查数据是否被其他线程修改过。如果被修改过则放弃本次操作或重试。
6.1 版本号机制
最常见的乐观锁实现方式:给表增加一个 version 字段,每次更新时检查版本号是否一致。
/**
* 乐观锁示例:使用版本号机制扣减库存
*/
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
/**
* 乐观锁扣减库存
*
* 执行流程:
* 1. 查询当前库存和版本号(不加锁)
* 2. 更新时带上版本号条件
* 3. 如果更新行数为0,说明被其他线程修改过
*
* SQL:
* UPDATE stock SET quantity = quantity - 1, version = version + 1
* WHERE product_id = ? AND version = ?
*/
public boolean deductStockWithOptimisticLock(Long productId, Integer quantity) {
// 1. 查询当前数据(不加锁)
Stock stock = stockMapper.selectById(productId);
if (stock == null) {
throw new BusinessException("商品不存在");
}
if (stock.getQuantity() < quantity) {
throw new BusinessException("库存不足");
}
// 2. 更新时带上版本号
int rows = stockMapper.deductWithVersion(
productId,
quantity,
stock.getVersion() // WHERE version = ?
);
// 3. 更新行数为0 → 被其他线程抢先修改了
if (rows == 0) {
return false; // 操作失败
}
return true; // 操作成功
}
}
对应的Mapper:
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
/**
* 乐观锁更新:WHERE条件中加上version
*/
@Update("UPDATE stock SET quantity = quantity - #{quantity}, " +
"version = version + 1 " +
"WHERE product_id = #{productId} AND version = #{version}")
int deductWithVersion(@Param("productId") Long productId,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
}
6.2 乐观锁的重试机制
乐观锁更新失败后,通常需要重试:
@Service
public class StockService {
private static final int MAX_RETRY = 3;
/**
* 带重试的乐观锁扣减
*/
public boolean deductStockWithRetry(Long productId, Integer quantity) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
// 1. 查询最新数据
Stock stock = stockMapper.selectById(productId);
if (stock.getQuantity() < quantity) {
throw new BusinessException("库存不足");
}
// 2. 尝试更新
int rows = stockMapper.deductWithVersion(
productId, quantity, stock.getVersion()
);
if (rows > 0) {
return true; // 成功
}
// 3. 失败,重试
retryCount++;
// 可选:短暂休眠后重试
try {
Thread.sleep(10 * retryCount); // 递增等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 超过最大重试次数
throw new BusinessException("系统繁忙,请稍后重试");
}
}
6.3 CAS机制(Compare And Swap)
CAS是乐观锁在CPU层面的实现,Java的原子类就是基于CAS。
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
private static AtomicInteger stock = new AtomicInteger(100);
/**
* CAS扣减库存
*
* CAS操作包含三个操作数:
* 内存位置(V)、预期原值(A)、新值(B)
*
* 如果V处的值等于A,则将V处的值更新为B,返回true
* 如果V处的值不等于A(被其他线程修改了),则不更新,返回false
*/
public static boolean deductStock(int quantity) {
while (true) {
int current = stock.get(); // A:当前值
int newVal = current - quantity; // B:新值
if (newVal < 0) {
System.out.println("库存不足");
return false;
}
// CAS:如果current仍然等于内存中的值,则更新为newVal
if (stock.compareAndSet(current, newVal)) {
System.out.println(Thread.currentThread().getName() +
" 扣减成功,剩余: " + newVal);
return true;
}
// CAS失败,循环重试
System.out.println(Thread.currentThread().getName() + " CAS失败,重试...");
}
}
public static void main(String[] args) {
// 模拟10个线程并发扣减
for (int i = 0; i < 10; i++) {
new Thread(() -> deductStock(10), "线程-" + i).start();
}
}
}
CAS的ABA问题
ABA问题: 线程1 读取值 A 线程2 将值从 A 改为 B,又从 B 改回 A 线程1 执行CAS,发现值仍然是A,认为没被修改过,更新成功 → 但实际上值已经被改过了(A→B→A) 解决方案: 1. AtomicStampedReference:带版本号的原子引用 2. AtomicMarkableReference:带布尔标记的原子引用
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
/**
* 使用AtomicStampedReference解决ABA问题
* 每次修改都会增加stamp(版本号)
*/
private static AtomicStampedReference<Integer> stock =
new AtomicStampedReference<>(100, 0); // 初始值100,初始版本0
public static boolean deductStock(int quantity) {
while (true) {
int[] stampHolder = new int[1];
int current = stock.get(stampHolder); // 获取值和版本号
int stamp = stampHolder[0]; // 当前版本号
int newVal = current - quantity;
if (newVal < 0) return false;
// CAS同时检查值和版本号
if (stock.compareAndSet(current, newVal, stamp, stamp + 1)) {
return true;
}
}
}
}
6.4 MyBatis-Plus 内置乐观锁
MyBatis-Plus提供了乐观锁插件,自动处理版本号。
// 1. 实体类添加 @Version
@Data
@TableName("stock")
public class Stock {
@TableId(type = IdType.AUTO)
private Long id;
private Long productId;
private Integer quantity;
@Version // 乐观锁版本号
private Integer version;
}
// 2. 配置乐观锁插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁插件(必须在分页插件之前)
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
// 3. 使用方式
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
/**
* 插件自动处理版本号,无需手写SQL
* 生成的SQL:UPDATE stock SET quantity=?, version=version+1 WHERE id=? AND version=?
*/
public boolean updateStock(Stock stock) {
int rows = stockMapper.updateById(stock);
return rows > 0;
}
/**
* 注意:必须先查询再更新,不能直接new对象更新
*/
public boolean deductStock(Long productId, Integer quantity) {
Stock stock = stockMapper.selectById(productId); // 先查(拿到version)
stock.setQuantity(stock.getQuantity() - quantity);
return stockMapper.updateById(stock) > 0; // 再改(自动带version条件)
}
}
6.5 乐观锁的优缺点
优点: ✓ 并发性能好,不需要加锁等待 ✓ 不会产生死锁 ✓ 适合读多写少的场景 ✓ 实现简单(加个version字段) 缺点: ✗ 写冲突多时重试频繁,反而降低性能 ✗ 可能产生ABA问题(用AtomicStampedReference解决) ✗ 需要额外的版本号字段 ✗ 高并发下大量请求可能失败需要重试 ✗ 不适合写多读少的场景(冲突太多,不断重试)
7. 悲观锁 vs 乐观锁
7.1 核心区别
悲观锁的思路:先锁住,再操作("先小人后君子") → 我不相信别人,我先把门锁上再说 乐观锁的思路:先操作,提交时检查("先君子后小人") → 我相信不会有人来,但提交时我会检查有没有人动过
7.2 详细对比
| 对比项 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 操作前加锁 | 更新时检查 |
| 实现方式 | SELECT FOR UPDATE | 版本号 / CAS |
| 并发性能 | 低(串行执行) | 高(并行执行) |
| 适用场景 | 写多读少、冲突频繁 | 读多写少、冲突较少 |
| 死锁风险 | 有 | 无 |
| 数据安全 | 高(加锁保证) | 需要重试保证 |
| 代码复杂度 | 低 | 中(需要重试逻辑) |
| 数据库支持 | 原生SQL支持 | 需要额外字段/代码 |
| 典型应用 | 库存扣减、账户转账 | 信息更新、订单状态变更 |
7.3 如何选择
选择悲观锁: 1. 写操作多,读操作少 2. 冲突频率高(比如秒杀场景,大量线程同时扣减同一件商品的库存) 3. 对数据安全性要求极高 4. 不想处理重试逻辑 选择乐观锁: 1. 读操作多,写操作少 2. 冲突频率低(比如修改用户信息,很少有两个人同时修改同一个人的信息) 3. 对并发性能要求高 4. 可以接受重试
8. 分布式锁
当应用从单机扩展到集群后,JVM锁和数据库锁都管不到其他机器。分布式锁用于解决多台服务器之间的并发控制问题。
8.1 为什么需要分布式锁
单机时代: 一台服务器,一个JVM → synchronized / ReentrantLock 就够了 集群时代: 多台服务器,多个JVM → JVM锁只能管自己进程内的线程 → 数据库锁可以,但高并发下数据库压力大 → 需要分布式锁 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 服务器A │ │ 服务器B │ │ 服务器C │ │ JVM锁 │ │ JVM锁 │ │ JVM锁 │ │ 只管A │ │ 只管B │ │ 只管C │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ └──────────────┼──────────────┘ │ ┌──────┴──────┐ │ Redis / │ │ Zookeeper │ │ 分布式锁 │ └─────────────┘
8.2 分布式锁的实现方案
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Redis | SETNX + EXPIRE | 性能高,实现简单 | 主从切换可能丢锁 |
| Zookeeper | 临时顺序节点 | 强一致性,自动清理 | 性能不如Redis |
| Redisson | Redis + 看门狗 + Lua脚本 | 功能完善,开箱即用 | 依赖Redis |
8.3 分布式锁的应用场景
1. 秒杀/抢购 → 多台服务器同时处理同一商品的订单,需要分布式锁防止超卖 2. 分布式任务调度 → 多个节点竞争执行同一个定时任务,防止重复执行 3. 库存扣减(微服务架构) → 库存服务部署在多台机器上,需要分布式锁保证扣减的原子性 4. 订单防重 → 防止用户在不同服务器上重复提交订单 5. 配置修改 → 多台服务器同时修改同一份配置,需要分布式锁保证一致性
9. Redis分布式锁的演进
从最简单的实现到生产可用,逐步演进。
9.1 最简单的实现:SETNX
/**
* 最简单的分布式锁:SETNX
*
* 问题:
* 1. 没有设置过期时间,如果服务器宕机,锁永远不释放(死锁)
* 2. 没有防误删机制,可能删除别人的锁
*/
@Component
public class SimpleDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryLock(String key, String value) {
// SET key value NX(NX = Not Exists,只有key不存在时才设置)
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value);
return Boolean.TRUE.equals(result);
}
public void unlock(String key) {
redisTemplate.delete(key);
}
}
9.2 改进一:加上过期时间
/**
* 改进:加上过期时间,防止死锁
*
* 问题:加锁和设置过期时间不是原子操作
* 如果加锁成功后、设置过期时间前服务器宕机,还是会死锁
*/
@Component
public class ImprovedDistributedLock1 {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryLock(String key, String value, long expireSeconds) {
// SET key value NX EX(NX + EX = 原子操作)
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
// 问题:unlock直接delete,可能删除别人的锁
// 场景:锁过期了,其他线程获取了锁,当前线程执行完后delete了别人的锁
public void unlock(String key) {
redisTemplate.delete(key);
}
}
9.3 改进二:防止误删
/**
* 改进:释放锁时检查是否是自己加的锁
*
* 问题:检查和删除不是原子操作
* 如果检查完发现是自己的锁,但在delete之前锁过期了,还是会误删
*/
@Component
public class ImprovedDistributedLock2 {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryLock(String key, String value, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock(String key, String value) {
// 检查是否是自己的锁
String currentValue = redisTemplate.opsForValue().get(key);
if (value.equals(currentValue)) {
// 问题:检查和删除不是原子操作
// 在检查通过之后、删除之前,锁可能已经过期并被其他线程获取
redisTemplate.delete(key);
}
}
}
9.4 改进三:Lua脚本保证原子性
/**
* 改进:使用Lua脚本,将检查和删除变成原子操作
* 这是生产可用的Redis分布式锁实现
*/
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* SET key value NX EX(原子操作)
*/
public boolean tryLock(String key, String value, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁(Lua脚本保证原子性)
*
* Lua脚本逻辑:
* if redis.call('get', KEYS[1]) == ARGV[1] then
* return redis.call('del', KEYS[1])
* else
* return 0
* end
*
* 只有锁的持有者才能释放锁
*/
public boolean unlock(String key, String value) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
return result != null && result > 0;
}
/**
* 生成唯一的锁标识(UUID + 线程ID)
*/
public String generateLockValue() {
return UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
}
}
9.5 使用示例
@Service
public class SeckillService {
@Autowired
private RedisDistributedLock distributedLock;
@Autowired
private StockMapper stockMapper;
public boolean seckill(Long productId, Integer quantity) {
String lockKey = "lock:stock:" + productId;
String lockValue = distributedLock.generateLockValue();
try {
// 尝试获取锁,最多等待3秒,10秒后自动过期
boolean acquired = distributedLock.tryLock(lockKey, lockValue, 10);
if (!acquired) {
// 获取锁失败,说明其他线程正在处理
return false;
}
// 获取锁成功,执行业务逻辑
Stock stock = stockMapper.selectById(productId);
if (stock.getQuantity() < quantity) {
return false;
}
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
return true;
} finally {
// 释放锁
distributedLock.unlock(lockKey, lockValue);
}
}
}
9.6 还存在的问题
上面的实现已经可以在大多数场景使用,但还存在一个问题: 业务执行时间 > 锁的过期时间 线程A 获取锁(10秒过期) 线程A 开始执行业务 ──── 10秒后 ──── 锁自动过期! 线程B 获取到锁,开始执行 线程A 执行完毕,尝试释放锁 → 可能释放了线程B的锁(虽然Lua脚本会检查,但业务上已经出现了并发问题) 解决方案:看门狗机制(自动续期)
10. 看门狗机制
看门狗(Watchdog)是分布式锁的自动续期机制,解决锁过期但业务还没执行完的问题。
10.1 什么是看门狗
没有看门狗的问题: 线程A 获取锁(过期时间30秒) 线程A 开始执行业务(预期35秒完成) ──── 30秒后 ──── 锁自动过期! 线程B 获取到锁,开始执行 线程A 执行完毕,尝试释放锁 → 失败(Lua检查不是自己的锁了) 但此时线程A和线程B可能同时操作了数据 ❌ 有看门狗: 线程A 获取锁(过期时间30秒) 看门狗启动,每隔10秒检查一次 ──── 第10秒:线程A还在执行 → 续期到30秒 ──── 第20秒:线程A还在执行 → 续期到30秒 ──── 第30秒:线程A还在执行 → 续期到30秒 线程A 执行完毕,主动释放锁,看门狗停止 ✅
10.2 Redisson的看门狗(最常用)
Redisson是Java中最成熟的Redis客户端,内置了完善的看门狗机制。
@Service
public class RedissonLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 基础用法:lock() 不传过期时间,看门狗自动生效
* 默认锁过期时间30秒,每10秒续期一次(过期时间的1/3)
*/
public void processWithWatchdog(String businessKey) {
RLock lock = redissonClient.getLock("lock:" + businessKey);
try {
// 不指定leaseTime → 看门狗自动续期
lock.lock();
// 执行业务逻辑(即使执行很久,看门狗也会自动续期)
doLongRunningTask();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 释放锁,看门狗自动停止
}
}
}
/**
* 尝试获取锁(带等待超时)
* 等待最多waitTime秒,不指定leaseTime → 看门狗自动续期
*/
public boolean processWithTimeout(String businessKey, long waitTime) {
RLock lock = redissonClient.getLock("lock:" + businessKey);
try {
boolean acquired = lock.tryLock(waitTime, TimeUnit.SECONDS);
if (!acquired) {
return false; // 获取锁超时
}
// 看门狗已在后台自动续期
doLongRunningTask();
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* ⚠️ 指定了leaseTime时,看门狗不会生效!
* 10秒后锁自动释放,即使业务没执行完
*/
public void processWithoutWatchdog(String businessKey) {
RLock lock = redissonClient.getLock("lock:" + businessKey);
try {
// 指定了10秒过期 → 看门狗不生效
lock.lock(10, TimeUnit.SECONDS);
doLongRunningTask(); // 如果超过10秒,锁就没了
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void doLongRunningTask() {
// 模拟长时间任务
}
}
10.3 看门狗的工作原理
/**
* 看门狗原理示意(简化版)
* Redisson内部实现的大致逻辑
*/
public class WatchdogConcept {
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private volatile boolean running = true;
/**
* 启动看门狗
*/
public void startWatchdog(String lockKey, long leaseTime) {
// 每隔 leaseTime/3 的时间续期一次
long renewalInterval = leaseTime / 3;
executor.scheduleAtFixedRate(() -> {
if (!running) return;
try {
// 续期:重新设置过期时间
// 只有锁还被当前线程持有时才续期
// 使用Lua脚本保证原子性
renewLock(lockKey, leaseTime);
} catch (Exception e) {
// 续期失败(锁已不存在或已不属于当前线程)
running = false;
}
}, renewalInterval, renewalInterval, TimeUnit.SECONDS);
}
/**
* 停止看门狗(释放锁时调用)
*/
public void stopWatchdog() {
running = false;
}
private void renewLock(String lockKey, long leaseTime) {
// Lua脚本:检查锁是否还被当前线程持有,是则续期
// if redis.call('get', KEYS[1]) == ARGV[1] then
// return redis.call('expire', KEYS[1], ARGV[2])
// else
// return 0
// end
}
}
10.4 手动实现看门狗(不用Redisson)
@Component
public class ManualWatchdog {
@Autowired
private StringRedisTemplate redisTemplate;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "watchdog-" + count.incrementAndGet());
t.setDaemon(true);
return t;
}
});
private final Map<String, ScheduledFuture<?>> watchdogFutures = new ConcurrentHashMap<>();
/**
* 加锁并启动看门狗
*/
public boolean tryLockWithWatchdog(String key, String value, long expireSeconds) {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(acquired)) {
startWatchdog(key, value, expireSeconds);
return true;
}
return false;
}
/**
* 启动看门狗续期
*/
private void startWatchdog(String key, String value, long expireSeconds) {
long renewalInterval = expireSeconds / 3;
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
try {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value,
String.valueOf(expireSeconds)
);
if (result != null && result == 0) {
// 锁已不属于当前线程,停止看门狗
throw new RuntimeException("锁已释放");
}
} catch (Exception e) {
throw new RuntimeException("看门狗停止", e);
}
}, renewalInterval, renewalInterval, TimeUnit.SECONDS);
watchdogFutures.put(key, future);
}
/**
* 释放锁并停止看门狗
*/
public boolean unlockWithWatchdog(String key, String value) {
// 停止看门狗
ScheduledFuture<?> future = watchdogFutures.remove(key);
if (future != null) {
future.cancel(true);
}
// 释放锁(Lua脚本保证原子性)
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 > 0;
}
}
10.5 看门狗的使用场景
1. 任务执行时间不确定 → 调用第三方接口、大文件处理、复杂计算 2. 分布式定时任务 → 多个节点竞争执行任务,任务执行时间可能很长 3. 秒杀/抢购 → 涉及多步操作(检查库存→扣库存→扣余额→创建订单),总耗时不确定 4. 数据迁移/同步 → 大量数据处理,耗时不可预测
11. Zookeeper分布式锁简介
11.1 Zookeeper锁的原理
Zookeeper利用临时顺序节点实现分布式锁: 1. 客户端在/locks目录下创建临时顺序节点 /locks/lock-0000000001 /locks/lock-0000000002 /locks/lock-0000000003 2. 获取/locks下所有子节点,判断自己是否是最小的 - 是最小的 → 获取锁成功 - 不是最小的 → 监听比自己小的那个节点 3. 当比自己小的节点被删除时(锁释放),收到通知 - 重新判断自己是否最小 - 是最小的 → 获取锁成功 4. 客户端断开连接时,临时节点自动删除(自动释放锁)
11.2 Curator实现Zookeeper锁
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
@Service
public class ZookeeperLockService {
@Autowired
private CuratorFramework curatorClient;
/**
* Zookeeper分布式锁
*/
public void processWithZkLock(String businessKey) {
InterProcessMutex lock = new InterProcessMutex(
curatorClient, "/locks/" + businessKey
);
try {
// 获取锁,最多等待10秒
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
doBusiness();
} finally {
lock.release(); // 释放锁
}
}
} catch (Exception e) {
throw new BusinessException("获取锁失败", e);
}
}
private void doBusiness() {
// 业务逻辑
}
}
11.3 Redis锁 vs Zookeeper锁
| 对比项 | Redis分布式锁 | Zookeeper分布式锁 |
|---|---|---|
| 性能 | 高(内存操作) | 中(写磁盘) |
| 可靠性 | 主从切换可能丢锁 | 强一致性 |
| 自动释放 | 靠过期时间 | 临时节点,断连自动删除 |
| 看门狗 | Redisson内置 | 不需要(临时节点机制) |
| 适用场景 | 高性能、可容忍极小概率丢锁 | 强一致性、不能丢锁 |
| 运维成本 | 低(大多已有Redis) | 高(需要部署ZK集群) |
12. 锁的选型指南
12.1 按场景选型
场景1:单机多线程 → synchronized / ReentrantLock → 适用:同一个JVM内的并发控制 场景2:单库多事务(并发不高) → 乐观锁(版本号) → 适用:读多写少,冲突不频繁 场景3:单库多事务(并发高/冲突频繁) → 悲观锁(SELECT FOR UPDATE) → 适用:写多读少,冲突频繁 场景4:微服务/集群(需要跨节点控制) → Redis分布式锁(Redisson) → 适用:大多数分布式场景 场景5:微服务/集群(强一致性要求) → Zookeeper分布式锁 → 适用:不能容忍丢锁的场景 场景6:分布式锁 + 长时间任务 → 分布式锁 + 看门狗 → 适用:执行时间不确定的任务
12.2 一张图总结
并发控制 │ ┌──────────────┼──────────────┐ │ │ │ JVM进程内 数据库层 分布式系统 │ │ │ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │synchronized│ │ 悲观锁 │ │Redis锁 │ │ReentrantL. │ │ 乐观锁 │ │ZK锁 │ └──────────┘ └─────────┘ └────┬────┘ │ ┌────┴────┐ │ 看门狗 │ │自动续期 │ └─────────┘
13. 实战综合示例
13.1 电商秒杀完整示例
综合运用悲观锁、乐观锁、分布式锁、看门狗。
@Service
@Slf4j
public class SeckillService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StockMapper stockMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountMapper accountMapper;
/**
* 秒杀下单
*
* 技术栈:分布式锁(Redisson + 看门狗)+ 数据库事务 + 悲观锁
*/
public SeckillResultVO seckill(Long userId, Long productId) {
String lockKey = "seckill:lock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 1. 获取分布式锁(看门狗自动续期)
boolean acquired = lock.tryLock(3, TimeUnit.SECONDS);
if (!acquired) {
return SeckillResultVO.fail("系统繁忙,请稍后重试");
}
// 2. 执行秒杀逻辑(在分布式锁保护下)
return doSeckill(userId, productId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SeckillResultVO.fail("系统异常");
} finally {
// 3. 释放分布式锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 秒杀业务逻辑(在事务中执行)
*/
@Transactional(rollbackFor = Exception.class)
public SeckillResultVO doSeckill(Long userId, Long productId) {
// 1. 检查库存(悲观锁)
Stock stock = stockMapper.selectForUpdate(productId);
if (stock == null || stock.getQuantity() <= 0) {
return SeckillResultVO.fail("商品已售罄");
}
// 2. 检查用户是否已购买(防重复购买)
Integer count = orderMapper.countByUserAndProduct(userId, productId);
if (count > 0) {
return SeckillResultVO.fail("每人限购一件");
}
// 3. 扣减库存
stock.setQuantity(stock.getQuantity() - 1);
stockMapper.updateById(stock);
// 4. 扣减余额
Account account = accountMapper.selectForUpdateByUserId(userId);
if (account.getBalance().compareTo(stock.getPrice()) < 0) {
throw new BusinessException("余额不足");
}
account.setBalance(account.getBalance().subtract(stock.getPrice()));
accountMapper.updateById(account);
// 5. 创建订单
Order order = new Order();
order.setOrderNo("SK" + System.currentTimeMillis());
order.setUserId(userId);
order.setProductId(productId);
order.setTotalAmount(stock.getPrice());
order.setStatus(0);
orderMapper.insert(order);
log.info("秒杀成功,用户: {},订单: {}", userId, order.getOrderNo());
return SeckillResultVO.success(order.getOrderNo());
}
}
13.2 分布式锁工具类
/**
* 通用分布式锁工具类
*/
@Component
public class DistributedLockUtil {
@Autowired
private RedissonClient redissonClient;
/**
* 带返回值的分布式锁执行
* 看门狗自动续期
*/
public <T> T executeWithLock(String lockKey, long waitTime, Supplier<T> business) {
RLock lock = redissonClient.getLock(lockKey);
try {
boolean acquired = lock.tryLock(waitTime, TimeUnit.SECONDS);
if (!acquired) {
throw new BusinessException("获取锁超时");
}
return business.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取锁被中断", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 无返回值版本
*/
public void executeWithLock(String lockKey, long waitTime, Runnable business) {
executeWithLock(lockKey, waitTime, () -> {
business.run();
return null;
});
}
}
// 使用示例
@Service
public class SomeService {
@Autowired
private DistributedLockUtil lockUtil;
public void someMethod() {
lockUtil.executeWithLock("my:lock", 5, () -> {
// 被分布式锁保护的业务逻辑
// 看门狗自动续期,不用担心锁过期
doSomething();
});
}
}
13.3 JVM锁 + 数据库锁的配合
/**
* 单机环境下,JVM锁 + 数据库乐观锁双重保护
*/
@Service
public class LocalStockService {
private final ReentrantLock jvmLock = new ReentrantLock();
@Autowired
private StockMapper stockMapper;
/**
* JVM锁(防止同一JVM内的并发)
* + 数据库乐观锁(防止数据库层面的并发)
*
* 适用于:单机部署,但有多个线程同时操作
*/
public boolean deductStock(Long productId, Integer quantity) {
jvmLock.lock();
try {
// 乐观锁更新
Stock stock = stockMapper.selectById(productId);
if (stock.getQuantity() < quantity) {
return false;
}
int rows = stockMapper.deductWithVersion(
productId, quantity, stock.getVersion()
);
return rows > 0;
} finally {
jvmLock.unlock();
}
}
}
14. 常见面试题精选
Q1:synchronized和ReentrantLock的区别?
1. synchronized是关键字,JVM层面实现;ReentrantLock是类,JDK层面实现 2. synchronized自动释放锁;ReentrantLock必须手动unlock(finally中) 3. ReentrantLock支持可中断、超时、公平锁、多条件变量 4. synchronized有锁升级优化(偏向→轻量→重量) 5. 性能上JDK1.6后基本持平 6. 简单场景用synchronized,需要高级特性用ReentrantLock
Q2:什么是死锁,如何避免?
死锁:两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行 死锁的四个必要条件(破坏任一个即可避免死锁): 1. 互斥条件:资源不能共享 → 一般无法破坏 2. 持有并等待:持有A锁的同时等待B锁 → 一次性申请所有资源 3. 不可抢占:不能抢夺别人持有的锁 → 申请失败时释放已持有的锁 4. 循环等待:形成环形等待链 → 按固定顺序申请锁 实际做法: - 按固定顺序获取锁(如按ID从小到大) - 设置获取锁的超时时间 - 使用tryLock,获取不到就放弃
Q3:乐观锁的ABA问题是什么,如何解决?
ABA问题: 线程1读取值A 线程2将值从A改为B,又从B改回A 线程1执行CAS,发现值仍然是A,更新成功 → 但实际上值已经被改过了 解决方案: 1. AtomicStampedReference:带版本号的原子引用 每次修改都增加stamp,CAS时同时检查值和stamp 2. AtomicMarkableReference:带布尔标记的原子引用 适合只关心"是否被修改过"的场景 3. 数据库层面用version字段,天然不存在ABA问题 因为version只增不减
Q4:Redis分布式锁为什么要用Lua脚本释放?
释放锁的逻辑是:先检查是不是自己的锁,再删除
如果分成两步:
1. GET key → 检查value
2. DEL key
问题:在步骤1和步骤2之间,锁可能已经过期并被其他线程获取
此时步骤2会删除其他线程的锁
Lua脚本将检查和删除变成一个原子操作:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
Redis执行Lua脚本是原子的,不会被其他命令打断
Q5:看门狗的续期时间为什么是过期时间的1/3?
Redisson默认:锁过期时间30秒,每10秒续期一次 为什么是1/3? - 太频繁(如每1秒):增加Redis压力 - 太稀疏(如每29秒):如果某次续期失败,只剩1秒的容错时间 - 1/3:即使某次续期失败,还有2/3的时间容错 即一次续期失败后,锁还有20秒才过期,足够重试或完成业务
15. 总结
三层锁的对比
┌──────────┬──────────────┬────────────────┬─────────────────┐ │ │ JVM锁 │ 数据库锁 │ 分布式锁 │ ├──────────┼──────────────┼────────────────┼─────────────────┤ │ 管辖范围 │ 单个JVM进程 │ 单个数据库 │ 所有节点 │ │ 实现方式 │ synchronized │ FOR UPDATE │ Redis/ZK │ │ │ ReentrantL. │ 版本号 │ Redisson │ │ 性能 │ 最高 │ 中等 │ 较高 │ │ 适用场景 │ 单机多线程 │ 单库多事务 │ 多机多节点 │ │ 配套机制 │ 无 │ 事务 │ 看门狗 │ └──────────┴──────────────┴────────────────┴─────────────────┘
最佳实践
1. 单机能解决的,不要用分布式 2. 乐观锁能解决的,不要用悲观锁 3. 悲观锁的SQL必须走索引,否则退化为表锁 4. 分布式锁一定要用Lua脚本释放 5. 分布式锁一定要用finally释放 6. Redisson不传leaseTime,让看门狗自动续期 7. 锁的粒度要尽可能细(如锁商品ID而不是锁整个库存表) 8. 按固定顺序获取多把锁,避免死锁 9. 永远不要在锁内做远程调用(除非有看门狗)
一句话总结
JVM锁管进程内,并发最高;悲观锁/乐观锁管数据库,用事务配合;分布式锁管集群,用看门狗续期。三层锁解决同一类问题——并发控制,只是管辖范围不同。理解它们的适用场景和配合方式,是构建高并发Java后端系统的基本功。
更多推荐



所有评论(0)