Skip to content
第 195 / 250 章中间件⏱ 12 分钟阅读

第 195 章:Redis 分布式锁

学习目标

  • 理解分布式锁原理
  • 掌握 Redisson 实战
  • 学会可重入锁、公平锁
  • 了解 RedLock 算法

一、为什么需要分布式锁

单机用 synchronized / ReentrantLock 就够,但多机部署时锁状态不共享,需要分布式锁。

二、分布式锁基本要求

特性说明
互斥任何时刻只能一个客户端持有
不死锁持锁方崩溃,锁能释放
容错Redis 部分节点挂了,锁仍可用
可重入同一线程可多次获取
公平按顺序获取(可选)

三、最简单的 SETNX 实现

3.1 加锁

java
public boolean tryLock(String key, String value, long expireMs) {
    Boolean ok = redisTemplate.opsForValue().setIfAbsent(key, value, expireMs, TimeUnit.MILLISECONDS);
    return Boolean.TRUE.equals(ok);
}

关键

  • 必须用 SET key value NX PX expireMs 单条命令(原子)
  • 不能先 SETNXEXPIRE(两条命令,中间崩溃就死锁)

3.2 解锁(必须验证 value)

java
private static final String UNLOCK_LUA =
    "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
    "  return redis.call('DEL', KEYS[1]) " +
    "else " +
    "  return 0 " +
    "end";

public void unlock(String key, String value) {
    redisTemplate.execute(new DefaultRedisScript<>(UNLOCK_LUA, Long.class),
        Collections.singletonList(key), value);
}

不要这样做

java
// ❌ 没校验 value,可能误删别人的锁
public void unlock(String key) {
    redisTemplate.delete(key);
}

3.3 完整实现

java
public class SimpleRedisLock {

    private static final String UNLOCK_SCRIPT =
        "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
        "return redis.call('DEL', KEYS[1]) else return 0 end";

    private final RedisTemplate<String, String> redisTemplate;
    private final String lockKey;
    private final String lockValue;
    private final long expireMs;
    private volatile boolean locked = false;

    public SimpleRedisLock(RedisTemplate<String, String> redisTemplate,
                          String lockKey, long expireMs) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString();
        this.expireMs = expireMs;
    }

    public boolean tryLock() {
        Boolean ok = redisTemplate.opsForValue().setIfAbsent(
            lockKey, lockValue, expireMs, TimeUnit.MILLISECONDS);
        if (Boolean.TRUE.equals(ok)) {
            locked = true;
            return true;
        }
        return false;
    }

    public void unlock() {
        if (!locked) return;
        redisTemplate.execute(new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
            Collections.singletonList(lockKey), lockValue);
    }
}

四、Redisson(企业首选)

4.1 引入

xml
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.23.4</version>
</dependency>

4.2 配置

yaml
spring:
  redis:
    host: localhost
    port: 6379
java
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://localhost:6379")
            .setPassword("yourpass")
            .setDatabase(0);
        return Redisson.create(config);
    }
}

4.3 基础用法

java
@Autowired
private RedissonClient redissonClient;

public void doBusiness() {
    RLock lock = redissonClient.getLock("order:lock:123");

    try {
        // 尝试加锁,最多等 5s,锁 10s 自动过期
        boolean ok = lock.tryLock(5, 10, TimeUnit.SECONDS);
        if (!ok) {
            throw new BusinessException("系统繁忙");
        }

        // 业务逻辑
        orderService.process();

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

4.4 锁特性

4.5 看门狗(WatchDog)

自动续期:锁默认 30s,后台线程每 10s 检查,业务没完就续到 30s。

java
// 默认行为
lock.tryLock();  // 不传时间 → 用看门狗(30s 锁,后台续期)
// lock.lock();    // 阻塞等,默认 30s

// 不用看门狗
lock.tryLock(5, 10, TimeUnit.SECONDS);  // 显式 10s,到期自动释放

4.6 公平锁

按请求顺序排队:

java
RLock fairLock = redissonClient.getFairLock("resource:1");
fairLock.lock();
try {
    // 业务
} finally {
    fairLock.unlock();
}

4.7 读写锁

java
RReadWriteLock rwLock = redissonClient.getReadWriteLock("cache:user");

// 读锁(可并发)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
    return redisTemplate.opsForValue().get("user:1");
} finally {
    readLock.unlock();
}

// 写锁(排他)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
    redisTemplate.opsForValue().set("user:1", user);
} finally {
    writeLock.unlock();
}

4.8 信号量(Semaphore)

java
RSemaphore semaphore = redissonClient.getSemaphore("resource");
// 限流:同时最多 10 个
boolean ok = semaphore.tryAcquire();
if (ok) {
    try {
        // 业务
    } finally {
        semaphore.release();
    }
}

4.9 CountDownLatch

java
RCountDownLatch latch = redissonClient.getCountDownLatch("task");
latch.trySetCount(3);

// 3 个子任务完成
latch.countDown();

// 主线程等
latch.await(10, TimeUnit.SECONDS);

五、RedLock 算法(高可用)

主从架构下,主挂了从还没同步,锁可能丢失。RedLock 用多个独立 Redis 实例:

java
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.1.1:6379");
RedissonClient client1 = Redisson.create(config1);

// ... 多个

RedissonRedLock redLock = new RedissonRedLock(
    lock1, lock2, lock3, lock4, lock5);

redLock.lock();
try {
    // 大多数(N/2 + 1)节点加锁成功才算成功
} finally {
    redLock.unlock();
}

性能

RedLock 性能差,大多数场景主从 + 自动续期足够

六、实战:秒杀场景

java
@Service
public class SeckillService {

    @Autowired
    private RedissonClient redisson;

    @Autowired
    private GoodsMapper goodsMapper;

    @Transactional
    public void seckill(Long userId, Long goodsId) {
        // 1. 分布式锁防超卖
        RLock lock = redisson.getLock("seckill:" + goodsId);
        try {
            if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
                throw new BusinessException("系统繁忙");
            }

            // 2. 查库存
            Goods goods = goodsMapper.selectById(goodsId);
            if (goods.getStock() <= 0) {
                throw new BusinessException("已售罄");
            }

            // 3. 扣库存
            goodsMapper.decrementStock(goodsId);

            // 4. 创建订单
            orderMapper.insert(new Order(userId, goodsId));

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

七、Redisson 最佳实践

java
// ✅ 用 tryLock 不用 lock(避免阻塞)
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { ... }

// ✅ 检查 isHeldByCurrentThread
if (lock.isHeldByCurrentThread()) {
    lock.unlock();
}

// ✅ 业务异常时锁会自动释放(过期时间到)

// ✅ 不要在锁里做耗时操作

八、本章小结

方案优点缺点
SETNX简单无续期、不公平
Redisson功能全、续期重量级
RedLock高可用性能差、复杂
锁类型用途
RLock默认可重入
RFairLock公平
RReadWriteLock读写分离
RSemaphore限流

动手练习

  1. 用 SETNX + Lua 实现一个分布式锁
  2. 集成 Redisson,实现订单处理
  3. 用 RReadWriteLock 优化缓存读写
  4. 用 RSemaphore 实现接口限流

推荐阅读


下一章:第 196 章:Redis 分布式 Session 与限流

本站基于 VitePress 构建 · 由 Codebook 团队维护