本文系统讲解Java后端开发中的锁机制,从JVM层的synchronized和ReentrantLock,到数据库层的悲观锁和乐观锁,再到分布式环境下的分布式锁和看门狗机制,配合大量实战代码,帮助你全面掌握并发控制的核心技术。


目录

  1. 什么是锁

  2. 锁的分类总览

  3. JVM层面的锁

  4. 数据库层面的锁

  5. 悲观锁

  6. 乐观锁

  7. 悲观锁 vs 乐观锁

  8. 分布式锁

  9. Redis分布式锁的演进

  10. 看门狗机制

  11. Zookeeper分布式锁简介

  12. 锁的选型指南

  13. 实战综合示例

  14. 常见面试题精选

  15. 总结


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后端系统的基本功。

更多推荐