🔐 分布式锁的三大门派:Redis、Zookeeper、数据库谁更强?

63 阅读15分钟

面试官:如何实现分布式锁?
候选人:用Redis的SETNX!
面试官:如果Redis挂了怎么办?锁超时了业务还没执行完怎么办?
候选人:😰💦(这...)

别慌!今天我们深入剖析分布式锁的三大流派,从原理到实战全搞定!


🎬 开篇:为什么需要分布式锁?

单机时代(一个人的江湖)

// JVM内的synchronized,轻松搞定
public synchronized void deductStock() {
    int stock = getStock();
    if (stock > 0) {
        setStock(stock - 1);
    }
}

分布式时代(群雄争霸)

        用户1请求 → 服务器A (JVM1) ─┐
        用户2请求 → 服务器B (JVM2) ─┤→ 同一个商品库存
        用户3请求 → 服务器C (JVM3) ─┘
        
问题:synchronized只能锁住单个JVM,无法跨服务器!
结果:超卖!😱

解决方案:需要一个所有服务器都能访问的"锁"!


🥇 第一章:Redis分布式锁 - 速度之王

方案一:基础版(不要用!❌)

// 错误示例:只用SETNX
public boolean tryLock(String key) {
    return redisTemplate.opsForValue().setIfAbsent(key, "locked");
}

public void unlock(String key) {
    redisTemplate.delete(key);
}

// 问题:如果程序崩溃,锁永远不会释放!💀

方案二:加超时(还不够!⚠️)

// 改进一点:加上过期时间
public boolean tryLock(String key, long expireTime) {
    Boolean result = redisTemplate.opsForValue()
        .setIfAbsent(key, "locked");
    if (result) {
        // 设置过期时间
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
        return true;
    }
    return false;
}

// 问题:SETNX和EXPIRE不是原子操作!
// 如果SETNX成功后,程序崩溃,还是会死锁!

方案三:原子操作(还不够!⚠️)

// 再改进:SET命令同时设置值和过期时间(原子操作)
public boolean tryLock(String key, long expireSeconds) {
    return redisTemplate.opsForValue()
        .setIfAbsent(key, "locked", expireSeconds, TimeUnit.SECONDS);
}

public void unlock(String key) {
    redisTemplate.delete(key);
}

// 问题:任何人都能删除锁!
// 场景:
// 1. 线程A获取锁
// 2. 线程A业务执行超时,锁过期自动释放
// 3. 线程B获取锁
// 4. 线程A业务完成,删除锁(但删的是线程B的锁!)

🌟 方案四:完整版(推荐!✅)

@Service
public class RedisDistributedLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LOCK_PREFIX = "distributed:lock:";
    
    /**
     * 加锁
     * @param lockKey 锁的key
     * @param requestId 请求ID(唯一标识,用于释放锁时校验)
     * @param expireTime 过期时间(秒)
     * @return 是否成功
     */
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        String key = LOCK_PREFIX + lockKey;
        
        // SET key value NX EX expireTime
        // NX:只在key不存在时设置
        // EX:设置过期时间(秒)
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS);
        
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放锁(使用Lua脚本保证原子性)
     */
    public boolean unlock(String lockKey, String requestId) {
        String key = LOCK_PREFIX + lockKey;
        
        // Lua脚本:先比较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<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(
            redisScript, 
            Collections.singletonList(key), 
            requestId
        );
        
        return Long.valueOf(1).equals(result);
    }
}

// 使用示例
@Service
public class StockService {
    
    @Autowired
    private RedisDistributedLock distributedLock;
    
    public void deductStock(Long productId) {
        String lockKey = "stock:" + productId;
        String requestId = UUID.randomUUID().toString();
        
        try {
            // 尝试加锁,超时时间10秒
            if (distributedLock.tryLock(lockKey, requestId, 10)) {
                try {
                    // 业务逻辑
                    int stock = getStock(productId);
                    if (stock > 0) {
                        setStock(productId, stock - 1);
                    }
                } finally {
                    // 释放锁
                    distributedLock.unlock(lockKey, requestId);
                }
            } else {
                throw new RuntimeException("获取锁失败");
            }
        } catch (Exception e) {
            log.error("扣减库存失败", e);
            throw e;
        }
    }
}

🎭 生活比喻:共享单车

1. 扫码开锁(tryLock):
   - 你扫码时,系统记录是"你"锁的车(requestId)
   - 设置30分钟超时(expireTime)

2. 骑行:
   - 只有你能骑这辆车
   - 其他人扫码会显示"车辆使用中"

3. 还车(unlock):
   - 你还车时,系统检查是不是你锁的
   - 只有你能还车,别人无法替你还

4. 超时处理:
   - 如果你骑了超过30分钟还没还车
   - 系统自动解锁(防止忘记还车导致永久锁定)

🔥 进阶:Redisson实现(自动续期!)

@Configuration
public class RedissonConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://localhost:6379")
            .setPassword("password");
        return Redisson.create(config);
    }
}

@Service
public class StockService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public void deductStock(Long productId) {
        String lockKey = "stock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待10秒,锁30秒后自动释放
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            
            if (locked) {
                try {
                    // Redisson的看门狗机制:
                    // 如果业务还在执行,会自动续期锁的过期时间!
                    Thread.sleep(40000); // 模拟长时间业务
                    // 锁不会过期,因为看门狗会每10秒续期一次!
                    
                    // 业务逻辑
                    int stock = getStock(productId);
                    if (stock > 0) {
                        setStock(productId, stock - 1);
                    }
                } finally {
                    lock.unlock(); // 手动释放锁
                }
            }
        } catch (InterruptedException e) {
            log.error("获取锁失败", e);
        }
    }
}

📊 Redisson看门狗机制

时间轴:
T0: 获取锁,过期时间30秒
T10: 看门狗检查:业务还在执行 → 续期到40秒
T20: 看门狗检查:业务还在执行 → 续期到50秒
T30: 看门狗检查:业务还在执行 → 续期到60秒
T35: 业务完成,手动释放锁

优点:不用担心业务执行时间超过锁过期时间!

🌐 进阶:RedLock算法(多Redis实例)

问题:单个Redis挂了怎么办?

方案:RedLock(使用多个独立的Redis实例)

算法:
1. 获取当前时间(毫秒)
2. 依次尝试从N个Redis实例获取锁
3. 如果从超过半数(N/2+1)的实例获取到锁,且总耗时<锁过期时间,则成功
4. 否则,释放所有已获取的锁

示例(5个Redis实例):
Redis1: ✅ 成功
Redis2: ❌ 失败
Redis3: ✅ 成功
Redis4: ✅ 成功(3/5 > 半数)
Redis5: ❌ 失败

→ 获取锁成功!
@Service
public class RedLockService {
    
    @Autowired
    private RedissonClient redisson1;
    @Autowired
    private RedissonClient redisson2;
    @Autowired
    private RedissonClient redisson3;
    @Autowired
    private RedissonClient redisson4;
    @Autowired
    private RedissonClient redisson5;
    
    public void businessWithRedLock(Long productId) {
        String lockKey = "stock:" + productId;
        
        // 创建RedLock(需要至少3个实例成功才算获取锁)
        RLock lock1 = redisson1.getLock(lockKey);
        RLock lock2 = redisson2.getLock(lockKey);
        RLock lock3 = redisson3.getLock(lockKey);
        RLock lock4 = redisson4.getLock(lockKey);
        RLock lock5 = redisson5.getLock(lockKey);
        
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
        
        try {
            // 尝试获取锁
            boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);
            if (locked) {
                try {
                    // 业务逻辑
                    deductStock(productId);
                } finally {
                    redLock.unlock();
                }
            }
        } catch (InterruptedException e) {
            log.error("获取RedLock失败", e);
        }
    }
}

⚖️ Redis锁的优缺点

✅ 优点

  1. 性能极高 👍👍👍
    • 内存操作,QPS可达10万+
  2. 实现简单
    • 几行代码搞定
  3. 支持自动过期
    • 不怕死锁

❌ 缺点

  1. 可靠性问题
    • Redis挂了,锁就失效
    • 主从切换可能丢失锁
  2. 时钟依赖
    • RedLock依赖系统时钟,时钟漂移可能出问题
性能评分:⭐⭐⭐⭐⭐ (5/5)
可靠性评分:⭐⭐⭐ (3/5)
适用场景:高性能、允许偶尔失效

🐘 第二章:Zookeeper分布式锁 - 可靠之王

核心原理:临时顺序节点 + Watch机制

ZooKeeper锁的实现:

/locks
  ├── lock_0000000001  (客户端A创建)
  ├── lock_0000000002  (客户端B创建)
  └── lock_0000000003  (客户端C创建)

规则:
1. 序号最小的客户端获得锁
2. 其他客户端监听前一个节点的删除事件
3. 前一个节点删除后,自己变成最小,获得锁

🎭 生活比喻:医院排队叫号

1. 取号:
   - 你到医院,取号机给你一个号码(创建临时顺序节点)
   - 号码:A001、A002、A003...

2. 等待:
   - 显示屏显示"正在叫号:A001"(序号最小的获得锁)
   - 你拿的是A003,需要等待

3. 监听:
   - 你只需要关注A002什么时候叫完(监听前一个节点)
   - 不用一直盯着显示屏

4. 轮到你:
   - A002叫完了(前一个节点删除)
   - 你变成最小号码,轮到你了(获得锁)

5. 看完病离开:
   - 你离开医院(释放锁,删除节点)
   - A004自动获得锁

💻 代码实现

@Component
public class ZookeeperDistributedLock {
    
    @Autowired
    private CuratorFramework zkClient;
    
    private static final String LOCK_PATH = "/distributed-locks";
    
    /**
     * 获取锁
     */
    public InterProcessMutex getLock(String lockName) {
        String lockPath = LOCK_PATH + "/" + lockName;
        return new InterProcessMutex(zkClient, lockPath);
    }
}

@Service
public class StockService {
    
    @Autowired
    private ZookeeperDistributedLock distributedLock;
    
    public void deductStock(Long productId) {
        InterProcessMutex lock = distributedLock.getLock("stock:" + productId);
        
        try {
            // 尝试获取锁,最多等待10秒
            if (lock.acquire(10, TimeUnit.SECONDS)) {
                try {
                    // 业务逻辑
                    int stock = getStock(productId);
                    if (stock > 0) {
                        setStock(productId, stock - 1);
                    }
                } finally {
                    // 释放锁
                    lock.release();
                }
            } else {
                throw new RuntimeException("获取锁超时");
            }
        } catch (Exception e) {
            log.error("扣减库存失败", e);
            throw new RuntimeException(e);
        }
    }
}

🔍 Zookeeper锁的工作流程

1. 客户端A请求锁:
   创建 /locks/stock_0000000001 (临时顺序节点)
   检查自己是否最小 → 是 → 获得锁 ✅

2. 客户端B请求锁:
   创建 /locks/stock_0000000002
   检查自己是否最小 → 否 → 等待
   监听 /locks/stock_0000000001 的删除事件

3. 客户端C请求锁:
   创建 /locks/stock_0000000003
   检查自己是否最小 → 否 → 等待
   监听 /locks/stock_0000000002 的删除事件

4. 客户端A释放锁:
   删除 /locks/stock_0000000001
   触发客户端B的Watch事件

5. 客户端B被唤醒:
   检查自己是否最小 → 是 → 获得锁 ✅
   
优点:避免"惊群效应"(每个客户端只监听前一个节点)

🛡️ 临时节点的保护机制

场景:客户端A获取锁后,服务器宕机

传统方案:
锁永久存在 → 死锁 ❌

Zookeeper方案:
1. 客户端A与Zookeeper保持心跳(Session)
2. 服务器宕机 → 心跳断开 → Session失效
3. Zookeeper自动删除临时节点
4. 客户端B自动获得锁 ✅

完美解决死锁问题!

⚖️ Zookeeper锁的优缺点

✅ 优点

  1. 可靠性高 👍👍👍
    • 基于Paxos/ZAB协议,强一致性
    • 临时节点自动删除,不会死锁
  2. 公平性好
    • 先来先得(FIFO)
    • 避免饥饿问题
  3. 无需手动设置超时
    • 自动检测客户端存活

❌ 缺点

  1. 性能较低
    • 写操作需要过半节点确认
    • QPS只有几千
  2. 运维复杂
    • 需要部署Zookeeper集群
    • 需要监控Session超时
  3. 依赖外部系统
    • Zookeeper挂了,锁服务不可用
性能评分:⭐⭐⭐ (3/5)
可靠性评分:⭐⭐⭐⭐⭐ (5/5)
适用场景:可靠性要求高、并发量中等

🗄️ 第三章:数据库分布式锁 - 传统之选

方案一:基于唯一索引

-- 创建锁表
CREATE TABLE distributed_lock (
    lock_key VARCHAR(64) NOT NULL COMMENT '锁的key',
    request_id VARCHAR(64) NOT NULL COMMENT '请求ID',
    expire_time DATETIME NOT NULL COMMENT '过期时间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (lock_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
@Repository
public interface DistributedLockMapper {
    
    /**
     * 尝试获取锁(利用主键唯一性)
     */
    @Insert("INSERT INTO distributed_lock (lock_key, request_id, expire_time) " +
            "VALUES (#{lockKey}, #{requestId}, #{expireTime})")
    int tryLock(@Param("lockKey") String lockKey,
                @Param("requestId") String requestId,
                @Param("expireTime") Date expireTime);
    
    /**
     * 释放锁(只能释放自己的锁)
     */
    @Delete("DELETE FROM distributed_lock " +
            "WHERE lock_key = #{lockKey} AND request_id = #{requestId}")
    int unlock(@Param("lockKey") String lockKey,
               @Param("requestId") String requestId);
    
    /**
     * 清理过期锁(定时任务)
     */
    @Delete("DELETE FROM distributed_lock WHERE expire_time < NOW()")
    int cleanExpiredLocks();
}

@Service
public class DatabaseDistributedLock {
    
    @Autowired
    private DistributedLockMapper lockMapper;
    
    /**
     * 尝试获取锁
     */
    public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
        try {
            Date expireTime = new Date(System.currentTimeMillis() + expireSeconds * 1000);
            int rows = lockMapper.tryLock(lockKey, requestId, expireTime);
            return rows > 0;
        } catch (DuplicateKeyException e) {
            // 主键冲突,说明锁已存在
            return false;
        }
    }
    
    /**
     * 释放锁
     */
    public boolean unlock(String lockKey, String requestId) {
        int rows = lockMapper.unlock(lockKey, requestId);
        return rows > 0;
    }
}

// 定时清理过期锁
@Component
public class LockCleanTask {
    
    @Autowired
    private DistributedLockMapper lockMapper;
    
    @Scheduled(fixedRate = 60000) // 每分钟执行一次
    public void cleanExpiredLocks() {
        int count = lockMapper.cleanExpiredLocks();
        if (count > 0) {
            log.info("清理过期锁:{} 个", count);
        }
    }
}

方案二:基于悲观锁(FOR UPDATE)

@Service
public class DatabasePessimisticLock {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 获取锁(使用FOR UPDATE)
     */
    @Transactional
    public void executeWithLock(String lockKey, Runnable business) {
        // 1. 查询锁记录,加行锁
        String sql = "SELECT * FROM distributed_lock WHERE lock_key = ? FOR UPDATE";
        try {
            jdbcTemplate.queryForMap(sql, lockKey);
        } catch (EmptyResultDataAccessException e) {
            // 记录不存在,插入
            jdbcTemplate.update(
                "INSERT INTO distributed_lock (lock_key, request_id) VALUES (?, ?)",
                lockKey, UUID.randomUUID().toString()
            );
        }
        
        // 2. 执行业务逻辑(在事务中,持有行锁)
        business.run();
        
        // 3. 事务提交,自动释放锁
    }
}

// 使用示例
@Service
public class StockService {
    
    @Autowired
    private DatabasePessimisticLock pessimisticLock;
    
    public void deductStock(Long productId) {
        String lockKey = "stock:" + productId;
        
        pessimisticLock.executeWithLock(lockKey, () -> {
            // 业务逻辑
            int stock = getStock(productId);
            if (stock > 0) {
                setStock(productId, stock - 1);
            }
        });
    }
}

🎭 生活比喻:停车位

方案一:停车牌(唯一索引)

1. 你开车到停车场
2. 尝试放置你的停车牌(INSERT)
3. 成功 → 你占用了车位 ✅
4. 失败(车位已被占用) → 等待或离开 ❌
5. 离开时取走停车牌(DELETE)

方案二:栏杆(FOR UPDATE)

1. 你开车到停车场
2. 栏杆挡住你(SELECT FOR UPDATE 加行锁)
3. 你停车、办事(执行业务)
4. 离开时栏杆抬起(事务提交,释放锁)
5. 下一辆车才能进入

⚖️ 数据库锁的优缺点

✅ 优点

  1. 实现简单
    • 不需要额外部署中间件
    • 利用现有的数据库
  2. 可靠性高
    • 基于事务,强一致性
  3. 易于理解和调试

❌ 缺点

  1. 性能最差 👎
    • 依赖数据库,QPS有限
    • 会增加数据库负载
  2. 死锁风险
    • FOR UPDATE可能导致死锁
  3. 单点故障
    • 数据库挂了,锁服务不可用
性能评分:⭐⭐ (2/5)
可靠性评分:⭐⭐⭐⭐ (4/5)
适用场景:并发量低、已有数据库

📊 第四章:三种方案全方位对比

维度RedisZookeeper数据库
性能⭐⭐⭐⭐⭐ 10万QPS⭐⭐⭐ 几千QPS⭐⭐ 几百QPS
可靠性⭐⭐⭐ 中等⭐⭐⭐⭐⭐ 很高⭐⭐⭐⭐ 高
实现复杂度简单中等简单
运维成本中等
死锁防护超时自动释放Session自动删除需定时清理
公平性不保证FIFO公平不保证
可重入需自己实现Curator自带需自己实现
阻塞锁需轮询Watch机制需轮询

🎯 选型决策树

graph TD
    A[开始选型] --> B{对性能要求极高?}
    B -->|是| C{能接受偶尔失效?}
    B -->|否| D{需要强一致性?}
    C -->|是| E[选择Redis]
    C -->|否| F[选择Zookeeper]
    D -->|是| G[选择Zookeeper]
    D -->|否| H{并发量大吗?}
    H -->|大| E
    H -->|小| I[选择数据库]

🎮 实际场景选择

场景推荐方案理由
秒杀系统Redis需要极高QPS
订单号生成Zookeeper需要严格顺序
定时任务调度Zookeeper需要可靠选主
库存扣减Redis + Redisson高性能+看门狗
分布式ID生成Zookeeper顺序性+可靠性
配置变更ZookeeperWatch机制
小型项目数据库简单够用

💼 第五章:生产环境最佳实践

实践1:混合使用

/**
 * 高可用方案:Redis主 + Zookeeper备
 */
@Service
public class HybridDistributedLock {
    
    @Autowired
    private RedissonClient redisson;
    @Autowired
    private CuratorFramework zkClient;
    
    public void executeWithLock(String lockKey, Runnable business) {
        RLock redisLock = redisson.getLock(lockKey);
        InterProcessMutex zkLock = new InterProcessMutex(zkClient, "/locks/" + lockKey);
        
        try {
            // 优先使用Redis锁(高性能)
            if (redisLock.tryLock(5, 30, TimeUnit.SECONDS)) {
                try {
                    business.run();
                } finally {
                    redisLock.unlock();
                }
            } else {
                // Redis获取失败,降级使用Zookeeper(高可靠)
                log.warn("Redis锁获取失败,降级使用Zookeeper锁");
                if (zkLock.acquire(10, TimeUnit.SECONDS)) {
                    try {
                        business.run();
                    } finally {
                        zkLock.release();
                    }
                }
            }
        } catch (Exception e) {
            log.error("执行业务失败", e);
            throw new RuntimeException(e);
        }
    }
}

实践2:锁续期(看门狗)

/**
 * 自己实现锁续期(如果不用Redisson)
 */
@Service
public class LockRenewalService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(1);
    
    public void executeWithAutoRenewal(String lockKey, Runnable business) {
        String requestId = UUID.randomUUID().toString();
        String key = "lock:" + lockKey;
        
        // 获取锁,30秒过期
        boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(key, requestId, 30, TimeUnit.SECONDS);
        
        if (!locked) {
            throw new RuntimeException("获取锁失败");
        }
        
        // 启动续期任务(每10秒续期一次)
        ScheduledFuture<?> renewalTask = scheduler.scheduleAtFixedRate(() -> {
            // 检查锁是否还是自己的
            String currentValue = redisTemplate.opsForValue().get(key);
            if (requestId.equals(currentValue)) {
                // 续期30秒
                redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                log.debug("锁续期成功:{}", lockKey);
            }
        }, 10, 10, TimeUnit.SECONDS);
        
        try {
            // 执行业务
            business.run();
        } finally {
            // 停止续期任务
            renewalTask.cancel(true);
            
            // 释放锁
            releaseLock(key, requestId);
        }
    }
    
    private void releaseLock(String key, String requestId) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        redisTemplate.execute(script, Collections.singletonList(key), requestId);
    }
}

实践3:锁降级

/**
 * 获取锁失败时的降级策略
 */
@Service
public class StockServiceWithDegradation {
    
    @Autowired
    private RedisDistributedLock distributedLock;
    @Autowired
    private StockMapper stockMapper;
    
    public void deductStock(Long productId, Integer quantity) {
        String lockKey = "stock:" + productId;
        String requestId = UUID.randomUUID().toString();
        
        // 尝试获取锁,等待时间0秒(不等待)
        if (distributedLock.tryLock(lockKey, requestId, 30)) {
            try {
                // 方案1:获取锁成功,正常扣减
                doDeductStock(productId, quantity);
            } finally {
                distributedLock.unlock(lockKey, requestId);
            }
        } else {
            // 方案2:获取锁失败,降级为数据库乐观锁
            log.warn("分布式锁获取失败,降级使用乐观锁");
            deductStockWithOptimisticLock(productId, quantity);
        }
    }
    
    private void deductStockWithOptimisticLock(Long productId, Integer quantity) {
        int maxRetry = 3;
        for (int i = 0; i < maxRetry; i++) {
            Stock stock = stockMapper.selectById(productId);
            if (stock.getQuantity() < quantity) {
                throw new StockNotEnoughException();
            }
            
            // 使用版本号乐观锁
            int rows = stockMapper.deductWithVersion(
                productId, quantity, stock.getVersion()
            );
            
            if (rows > 0) {
                return; // 成功
            }
            // 失败,重试
        }
        throw new RuntimeException("扣减库存失败");
    }
}

🎓 第六章:面试高分回答

问题:如何实现分布式锁?

标准回答(STAR法则):

S(场景):"我们的电商系统在秒杀活动时,多个服务器会同时扣减库存,存在超卖风险。"

T(任务):"需要实现一个分布式锁,保证同一时刻只有一个请求能扣减库存。"

A(方案):"我们对比了Redis、Zookeeper、数据库三种方案,最终选择了Redis + Redisson:

  1. 使用Redisson的RLock,自带看门狗机制,不用担心锁超时
  2. 使用Lua脚本保证释放锁的原子性
  3. 为了高可用,部署了Redis Sentinel主从模式
  4. 核心代码只需要几行:
    RLock lock = redisson.getLock(lockKey);
    lock.lock();
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
    

"

R(结果):"上线后,秒杀场景下没有出现超卖,性能也很好,锁的QPS达到了5万+。"

常见追问及回答

Q1:Redis主从切换时,锁会丢失吗?

A:会的!
场景:
1. 客户端A在主节点上获取锁
2. 主节点挂掉,还未同步到从节点
3. 从节点升级为主节点
4. 客户端B在新主节点上获取同一个锁(成功)
5. 两个客户端同时持有锁!

解决方案:
1. 使用RedLock算法(多个独立的Redis实例)
2. 使用Zookeeper替代
3. 业务层面做幂等性校验

Q2:Redisson的看门狗是怎么实现的?

A:
1. 默认锁过期时间30秒
2. 启动一个后台线程(看门狗)
3. 每隔10秒(lockWatchdogTimeout/3)检查一次
4. 如果锁还存在,续期到30秒
5. 直到业务执行完,手动unlock时停止续期

源码:
org.redisson.RedissonLock#scheduleExpirationRenewal

Q3:如何避免死锁?

A:三种方案都有应对措施:
1. Redis:设置过期时间,自动释放
2. Zookeeper:临时节点,Session失效自动删除
3. 数据库:定时任务清理过期锁

另外,代码层面:
- 一定要在finally中释放锁
- 使用try-with-resources
- 设置合理的超时时间

🎁 总结:一句话记住

  • Redis:跑车(快但可能翻车)🏎️
  • Zookeeper:坦克(慢但非常稳)🚜
  • 数据库:自行车(简单够用)🚲

📚 扩展阅读


记住:选择合适的方案,比追求完美更重要!🎯

祝你面试顺利!💪✨