应用锁与数据库锁
在并发编程和分布式系统中,锁是保证数据一致性和线程安全的核心机制。正确选择和使用锁对于系统性能和可靠性至关重要。
1. 应用层锁(JVM 内锁)
1.1 synchronized 关键字
// 方法级别锁
public synchronized void method() {
// 同步方法,锁住当前对象实例
}
// 代码块级别锁
public void method() {
synchronized(this) {
// 锁住当前对象实例
}
synchronized(SomeClass.class) {
// 锁住类对象(静态锁)
}
}
特点:
- JVM 内置,自动获取和释放
- 可重入:同一个线程可以多次获取同一把锁
- 非公平锁:不保证等待时间最长的线程优先获得锁
- 性能开销相对较小
1.2 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在finally中释放
}
}
// 公平锁
private final ReentrantLock fairLock = new ReentrantLock(true);
优势:
- 可中断:
lockInterruptibly()支持响应中断 - 可超时:
tryLock(timeout, unit)支持超时获取 - 可查询:
isLocked()、getHoldCount()等方法 - 公平性:可选择公平或非公平锁
1.3 ReadWriteLock
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public void read() {
readLock.lock();
try {
// 多个读线程可以同时访问
} finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
// 写操作独占访问
} finally {
writeLock.unlock();
}
}
适用场景:读多写少的场景,如缓存、配置管理等。
1.4 StampedLock(Java 8+)
private final StampedLock stampedLock = new StampedLock();
public void optimisticRead() {
long stamp = stampedLock.tryOptimisticRead();
// 乐观读,假设没有写操作
if (!stampedLock.validate(stamp)) {
// 验证失败,升级为悲观读
stamp = stampedLock.readLock();
try {
// 重新读取
} finally {
stampedLock.unlockRead(stamp);
}
}
}
优势:支持乐观读,性能更好;避免了读写锁的写饥饿问题。
2. 数据库锁
2.1 行级锁(Row-Level Locking)
InnoDB 行锁:
- 基于索引实现,只有通过索引条件检索数据才会使用行锁
- 如果不走索引,会升级为表锁
- 支持共享锁(S锁)和排他锁(X锁)
-- 共享锁(读锁)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 排他锁(写锁)
SELECT * FROM users WHERE id = 1 FOR UPDATE;
注意事项:
- 行锁只在事务中生效
- 范围查询可能导致间隙锁(Gap Lock)
- 死锁检测和处理需要应用层配合
2.2 表级锁(Table-Level Locking)
MyISAM 表锁:
- 读操作加读锁,写操作加写锁
- 写锁优先级高于读锁
- 并发性能较差,但开销小
-- 手动加表锁
LOCK TABLES users READ;
LOCK TABLES users WRITE;
UNLOCK TABLES;
2.3 意向锁(Intention Locks)
InnoDB 的意向锁用于表级和行级锁的协调:
| 意向锁类型 | 说明 |
|---|---|
| IS(Intention Shared) | 表示事务准备给某些行加共享锁 |
| IX(Intention Exclusive) | 表示事务准备给某些行加排他锁 |
意向锁是表级锁,与行锁兼容,主要用于快速判断表中是否有行被锁定。
2.4 间隙锁(Gap Lock)
防止幻读的锁机制:
-- 假设 users 表中有 id: 1, 3, 5
SELECT * FROM users WHERE id BETWEEN 2 AND 4 FOR UPDATE;
-- 会锁定 (1,3] 和 (3,5) 的间隙,防止插入 id=2,4 的记录
影响:
- REPEATABLE READ 隔离级别下默认启用
- 可能导致死锁概率增加
- 可通过设置隔离级别为 READ COMMITTED 来避免
3. 分布式锁
3.1 Redis 分布式锁
基础实现(有问题)
// ❌ 不安全的实现
public boolean tryLock(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
安全实现(Redisson)
// ✅ 使用 Redisson 的 RLock
RLock lock = redisson.getLock("myLock");
try {
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
// 执行业务逻辑
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
Redisson 实现原理:
- Lua 脚本保证原子性
- WatchDog 机制自动续期
- 可重入锁支持
- 支持 RedLock 算法(多 Redis 实例)
3.2 ZooKeeper 分布式锁
基于临时顺序节点实现:
// 使用 Curator Framework
InterProcessMutex lock = new InterProcessMutex(client, "/locks/my-lock");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
lock.release();
}
实现原理:
- 创建临时顺序节点
- 判断自己是否是最小序号节点
- 不是最小则监听前一个节点的删除事件
- 临时节点保证客户端断开时自动释放锁
3.3 数据库分布式锁
基于唯一约束
-- 创建锁表
CREATE TABLE distributed_lock (
lock_name VARCHAR(100) PRIMARY KEY,
lock_value VARCHAR(100) NOT NULL
);
-- 获取锁
INSERT INTO distributed_lock(lock_name, lock_value) VALUES('order_lock', 'client_1');
-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'order_lock';
优点:简单可靠,利用数据库 ACID 特性 缺点:性能较差,无法自动过期
基于版本号
-- 获取锁(带超时)
UPDATE distributed_lock
SET lock_value = 'client_1', expire_time = NOW() + INTERVAL 30 SECOND
WHERE lock_name = 'order_lock' AND (expire_time < NOW() OR lock_name IS NULL);
-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND lock_value = 'client_1';
4. 锁的选择策略
4.1 单机应用
- 简单场景:synchronized
- 需要高级特性:ReentrantLock
- 读多写少:ReadWriteLock 或 StampedLock
4.2 分布式应用
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 高性能要求 | Redis 分布式锁 | 性能好,支持自动过期 |
| 强一致性要求 | ZooKeeper 分布式锁 | 顺序一致性,可靠性高 |
| 已有数据库 | 数据库分布式锁 | 无需引入新组件 |
| 跨语言环境 | ETCD 分布式锁 | gRPC 接口,多语言支持 |
4.2 性能对比
- 应用层锁:纳秒级,最快
- Redis 锁:毫秒级,网络延迟影响
- ZooKeeper 锁:几十毫秒级,强一致性代价
- 数据库锁:几十毫秒级,受数据库性能影响
5. 常见问题与最佳实践
5.1 死锁预防
应用层死锁:
// ❌ 可能死锁
synchronized(lockA) {
synchronized(lockB) {
// ...
}
}
synchronized(lockB) {
synchronized(lockA) {
// ...
}
}
// ✅ 按固定顺序获取锁
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
// 总是先获取 lock1,再获取 lock2
数据库死锁:
- 尽量按相同顺序访问表和行
- 减少事务持有锁的时间
- 使用合适的隔离级别
- 设置合理的死锁超时时间
5.2 锁粒度控制
- 粗粒度锁:简单但并发性能差
- 细粒度锁:并发性能好但复杂度高
- 分段锁:折中方案,如 ConcurrentHashMap
// 分段锁示例
private final Map<String, Object> segmentLocks = new ConcurrentHashMap<>();
public void process(String key) {
Object segmentLock = segmentLocks.computeIfAbsent(
key.substring(0, Math.min(2, key.length())),
k -> new Object()
);
synchronized(segmentLock) {
// 处理逻辑
}
}
5.3 超时与重试
public boolean acquireLockWithRetry(String lockKey, int maxRetries, long retryInterval) {
for (int i = 0; i < maxRetries; i++) {
if (tryAcquireLock(lockKey)) {
return true;
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
5.4 监控与告警
- 锁等待时间:监控获取锁的平均等待时间
- 锁持有时间:监控临界区执行时间
- 死锁检测:定期检查死锁情况
- 锁竞争:监控锁的竞争激烈程度
6. 实际应用场景
6.1 库存扣减
// Redis 分布式锁 + 数据库乐观锁
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
String lockKey = "stock_lock:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 查询当前库存
Product product = productMapper.selectById(productId);
if (product.getStock() >= quantity) {
// 乐观锁更新
int updated = productMapper.updateStockWithVersion(
productId, quantity, product.getVersion()
);
return updated > 0;
}
}
} finally {
lock.unlock();
}
return false;
}
6.2 订单号生成
// 数据库自增 + 应用层缓存
@Component
public class OrderIdGenerator {
private final AtomicInteger counter = new AtomicInteger(0);
private volatile long currentBatch = 0L;
private final Object batchLock = new Object();
public long nextId() {
int count = counter.getAndIncrement();
if (count % 1000 == 0) { // 每1000个ID重新获取批次
synchronized(batchLock) {
if (counter.get() % 1000 == 0) {
currentBatch = fetchNextBatchFromDB();
counter.set(1);
}
}
}
return currentBatch + count % 1000;
}
}
6.3 缓存更新
// 双重检查锁 + 本地缓存
private final Map<String, Object> localCache = new ConcurrentHashMap<>();
private final Map<String, Object> cacheLocks = new ConcurrentHashMap<>();
public Object getData(String key) {
Object data = localCache.get(key);
if (data == null) {
Object lock = cacheLocks.computeIfAbsent(key, k -> new Object());
synchronized(lock) {
data = localCache.get(key);
if (data == null) {
data = loadFromRemote(key);
localCache.put(key, data);
}
}
}
return data;
}
总结:锁的选择需要根据具体场景权衡性能、一致性和复杂度。单机场景优先考虑 JVM 内置锁,分布式场景根据一致性要求选择合适的分布式锁实现。无论哪种锁,都要注意死锁预防、超时处理和监控告警。