口语八股——Redis 面试实战指南(二):缓存篇、分布式锁篇

一、缓存篇 - 重中之重!

1.1 缓存穿透、击穿、雪崩是什么?怎么解决?

这是Redis面试的必考题!也是实际工作中最容易遇到的问题!我详细说明:


一、缓存穿透(Cache Penetration)

1. 什么是缓存穿透?

用户请求的数据,既不在缓存里,也不在数据库里。导致每次请求都会穿透缓存,直接打到数据库。

举例:

复制代码
用户查询id=-1的商品
→ Redis里没有
→ 去MySQL查,也没有
→ 返回空

如果有黑客不断用不存在的id查询,每次都会打到数据库,数据库可能被拖垮!

2. 怎么判断是穿透?

特征:

  • 请求的key在Redis和数据库都不存在
  • 大量这种请求会拖垮数据库
  • 通常是恶意攻击

3. 解决方案

方案一: 缓存空对象/缺省值

java 复制代码
public Product getProduct(Long id) {
    // 1. 先查Redis
    String cacheKey = "product:" + id;
    String productJson = redisTemplate.opsForValue().get(cacheKey);
    
    if (productJson != null) {
        // 2. 缓存命中
        if ("null".equals(productJson)) {
            return null;  // 之前查过,不存在
        }
        return JSON.parseObject(productJson, Product.class);
    }
    
    // 3. 缓存未命中,查数据库
    Product product = productMapper.selectById(id);
    
    if (product == null) {
        // 4. 数据库也没有,缓存一个空值,防止穿透
        redisTemplate.opsForValue().set(cacheKey, "null", 5, TimeUnit.MINUTES);
        return null;
    }
    
    // 5. 数据库有,缓存起来
    redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
    return product;
}

优点 : 简单,实现快 缺点:

  • 会占用Redis内存(大量不存在的key)
  • 如果攻击者每次用不同的id,还是会穿透

方案二: 布隆过滤器(Bloom Filter) - 推荐!

原理 : 布隆过滤器是一个很长的二进制向量,可以快速判断一个元素一定不存在 或者可能存在

java 复制代码
@Configuration
public class BloomFilterConfig {
    
    @Bean
    public RBloomFilter<String> bloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("product:bloom");
        // 初始化:预计10万个元素,误判率1%
        bloomFilter.tryInit(100000L, 0.01);
        return bloomFilter;
    }
}

@Service
public class ProductService {
    
    @Autowired
    private RBloomFilter<String> bloomFilter;
    
    // 启动时把所有商品id加入布隆过滤器
    @PostConstruct
    public void init() {
        List<Long> allProductIds = productMapper.selectAllIds();
        for (Long id : allProductIds) {
            bloomFilter.add("product:" + id);
        }
    }
    
    public Product getProduct(Long id) {
        String cacheKey = "product:" + id;
        
        // 1. 先用布隆过滤器判断
        if (!bloomFilter.contains(cacheKey)) {
            // 一定不存在,直接返回null,连Redis都不用查!
            return null;
        }
        
        // 2. 可能存在,查Redis
        String productJson = redisTemplate.opsForValue().get(cacheKey);
        if (productJson != null) {
            return JSON.parseObject(productJson, Product.class);
        }
        
        // 3. Redis没有,查数据库
        Product product = productMapper.selectById(id);
        if (product != null) {
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        }
        
        return product;
    }
}

优点:

  • 内存占用极小(10亿key只需要1.2GB,而Set需要几十GB)
  • 查询速度快(O(k),k是哈希函数个数,一般是个位数)

缺点:

  • 有一定误判率(可以通过调整参数降低到0.01%)
  • 不支持删除(有些实现支持计数布隆过滤器)

方案三: 接口校验

java 复制代码
// 在接口层做参数校验
if (id == null || id <= 0) {
    throw new IllegalArgumentException("商品id不合法");
}

4. 实际项目经验

我们的电商项目,曾经被攻击过,大量查询不存在的商品id,QPS瞬间上万,MySQL差点挂了。

解决方案:

  1. 用Redisson的布隆过滤器,把100万+商品id加载进去
  2. 在网关层限流,同一IP 1秒内最多10次请求
  3. 对明显非法的参数(负数、特别大的数)直接拒绝

上线后,攻击流量被布隆过滤器拦下了99%,数据库压力骤降。


二、缓存击穿(Cache Breakdown/Hotspot Invalid)

1. 什么是缓存击穿?

一个热点key突然过期,这时有大量请求访问这个key,都打到了数据库。

和穿透的区别:

  • 穿透: key不存在
  • 击穿: key存在,但是过期了,而且是热点数据

举例:

复制代码
双11秒杀活动,iPhone 15 Pro的库存信息缓存在Redis,key是"product:iphone15pro:stock"
设置了过期时间10分钟

10:00:00 key过期
10:00:01 10万用户同时查询库存
→ Redis里没有(刚过期)
→ 10万请求同时打到MySQL查库存
→ MySQL扛不住,挂了!

2. 解决方案

方案一: 热点数据永不过期

java 复制代码
// 不设置过期时间
redisTemplate.opsForValue().set("product:iphone15pro:stock", "1000");

// 或者设置一个逻辑过期时间
ProductCache cache = new ProductCache();
cache.setData(product);
cache.setExpireTime(System.currentTimeMillis() + 10 * 60 * 1000); // 10分钟后逻辑过期
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(cache));

// 查询时判断逻辑过期时间
ProductCache cache = JSON.parseObject(cacheJson, ProductCache.class);
if (cache.getExpireTime() < System.currentTimeMillis()) {
    // 过期了,异步刷新
    threadPool.submit(() -> refreshCache(id));
    // 先返回旧数据
    return cache.getData();
}

优点 : 完全避免击穿 缺点:

  • 可能返回过期数据
  • 内存占用,数据不会自动清理

方案二: 互斥锁(Mutex) - 最常用!

思路: 缓存失效时,不是所有请求都去查数据库,而是只有一个请求去查,其他请求等待。

用SETNX实现:

java 复制代码
public Product getProductWithMutex(Long id) {
    String cacheKey = "product:" + id;
    String lockKey = "lock:product:" + id;
    
    // 1. 查缓存
    String productJson = redisTemplate.opsForValue().get(cacheKey);
    if (productJson != null) {
        return JSON.parseObject(productJson, Product.class);
    }
    
    // 2. 缓存未命中,尝试获取锁
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    try {
        if (locked) {
            // 3. 获取到锁,查数据库
            Product product = productMapper.selectById(id);
            if (product != null) {
                // 4. 写缓存
                redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
            }
            return product;
        } else {
            // 5. 没获取到锁,等一会再查缓存
            Thread.sleep(100);
            return getProductWithMutex(id); // 递归重试
        }
    } finally {
        // 6. 释放锁
        if (locked) {
            redisTemplate.delete(lockKey);
        }
    }
}

用Redisson实现(更优雅):

java 复制代码
@Autowired
private RedissonClient redissonClient;

public Product getProductWithRedisson(Long id) {
    String cacheKey = "product:" + id;
    String lockKey = "lock:product:" + id;
    
    // 1. 查缓存
    String productJson = redisTemplate.opsForValue().get(cacheKey);
    if (productJson != null) {
        return JSON.parseObject(productJson, Product.class);
    }
    
    // 2. 缓存未命中,加锁
    RLock lock = redissonClient.getLock(lockKey);
    try {
        lock.lock(10, TimeUnit.SECONDS); // 10秒自动释放,防止死锁
        
        // 3. 再次查缓存(可能其他线程已经加载了)
        productJson = redisTemplate.opsForValue().get(cacheKey);
        if (productJson != null) {
            return JSON.parseObject(productJson, Product.class);
        }
        
        // 4. 查数据库
        Product product = productMapper.selectById(id);
        if (product != null) {
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        }
        return product;
    } finally {
        lock.unlock();
    }
}

优点:

  • 完全避免大量请求打到数据库
  • 数据一定是最新的

缺点:

  • 性能稍差(没拿到锁的请求要等待)
  • 实现稍复杂

方案三: 提前刷新

java 复制代码
// 定时任务,在热点数据快过期时主动刷新
@Scheduled(fixedRate = 60000) // 每分钟执行
public void refreshHotKeys() {
    List<String> hotKeys = getHotKeys(); // 获取热点key列表
    for (String key : hotKeys) {
        Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        if (ttl != null && ttl < 300) { // 剩余时间小于5分钟
            // 主动刷新
            Product product = productMapper.selectById(getIdFromKey(key));
            redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        }
    }
}

3. 实际项目选择

我的项目中:

  • 普通热点数据: 用互斥锁(Redisson)
  • 超级热点数据: 永不过期 + 异步刷新
  • 秒杀商品: 提前预热 + 永不过期

三、缓存雪崩(Cache Avalanche)

1. 什么是缓存雪崩?

大量key在同一时间过期 ,或者Redis服务宕机,导致大量请求打到数据库。

和击穿的区别:

  • 击穿: 一个热点key过期
  • 雪崩: 大量key同时过期

举例:

复制代码
电商首页缓存了100个商品分类的数据,都设置了30分钟过期

10:00:00 100个key同时过期
10:00:01 首页请求涌入
→ 100个分类都要查数据库
→ MySQL瞬间几千QPS,直接挂了
→ 所有请求失败,雪崩了!

2. 解决方案

方案一: 过期时间打散(加随机值) - 最常用!

java 复制代码
// ❌ 错误:所有key都是30分钟
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);

// ✅ 正确:加上0-5分钟的随机值
Random random = new Random();
int randomSeconds = random.nextInt(300); // 0-300秒
redisTemplate.opsForValue().set(key, value, 30 * 60 + randomSeconds, TimeUnit.SECONDS);

原理: 让key的过期时间分散在30-35分钟之间,不会同时过期。

方案二: 永不过期 + 异步刷新

java 复制代码
// 和击穿的方案一样
redisTemplate.opsForValue().set(key, value); // 不设置过期时间

// 用定时任务或者逻辑过期时间异步刷新

方案三: 多级缓存

复制代码
本地缓存(Caffeine/Guava Cache)
       ↓ 未命中
Redis缓存
       ↓ 未命中
MySQL数据库
@Autowired
private LoadingCache<String, Product> localCache; // Caffeine本地缓存

public Product getProduct(Long id) {
    String cacheKey = "product:" + id;
    
    // 1. 先查本地缓存
    Product product = localCache.getIfPresent(cacheKey);
    if (product != null) {
        return product;
    }
    
    // 2. 再查Redis
    String productJson = redisTemplate.opsForValue().get(cacheKey);
    if (productJson != null) {
        product = JSON.parseObject(productJson, Product.class);
        localCache.put(cacheKey, product); // 写入本地缓存
        return product;
    }
    
    // 3. 最后查数据库
    product = productMapper.selectById(id);
    if (product != null) {
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        localCache.put(cacheKey, product);
    }
    
    return product;
}

优点 : 即使Redis挂了,本地缓存还能扛一部分流量 缺点: 本地缓存可能有数据不一致问题

方案四: Redis集群 + 哨兵

复制代码
Master
  ↓
Slave1  Slave2  Slave3
  ↓
Sentinel(哨兵,监控)

原理: 即使Master挂了,哨兵会自动把Slave提升为Master,服务不中断。

方案五: 熔断降级

java 复制代码
// 用Hystrix或Sentinel实现熔断
@SentinelResource(value = "getProduct", fallback = "getProductFallback")
public Product getProduct(Long id) {
    // 正常逻辑
}

// 降级方法:返回默认值或从数据库查
public Product getProductFallback(Long id, Throwable e) {
    log.error("Redis异常,降级处理", e);
    // 返回默认商品信息或者查数据库
    return getDefaultProduct();
}

3. 实际项目经验

我们去年双11期间,凌晨0点活动开始,大量缓存同时失效(之前没加随机值),导致MySQL QPS瞬间从1000涨到2万,数据库差点挂了。

紧急处理:

  1. 立即重启部分应用,减少请求量
  2. 开启Sentinel限流,QPS限制在5000
  3. 手动刷新热点数据到Redis

后续优化:

  1. 所有缓存过期时间都加随机值
  2. 热点数据提前预热,永不过期
  3. 加入Caffeine本地缓存
  4. 加强监控,缓存命中率低于90%就告警

四、三者对比总结表
问题 原因 表现 解决方案
缓存穿透 查询不存在的数据 大量请求打到DB 布隆过滤器、缓存空值、参数校验
缓存击穿 热点key过期 瞬间大量请求打到DB 互斥锁、永不过期、提前刷新
缓存雪崩 大量key同时过期或Redis宕机 DB压力剧增,可能崩溃 过期时间打散、集群、降级、多级缓存

💡 记忆技巧:

  • 穿透: 不存在穿过缓存 → 布隆过滤器挡住
  • 击穿: 一个热点key被击穿 → 加锁排队
  • 雪崩: 大量key崩塌 → 打散过期时间

💡 面试加分项: 一定要结合实际项目说!比如"我们双11期间就遇到过雪崩,后来怎么解决的",这样面试官会觉得你真正处理过生产问题。


1.2 如何保证缓存和数据库的一致性?

✅ 正确回答思路:

这是个非常经典且复杂的问题,我先说结论:很难做到强一致性,通常只能保证最终一致性

一、常见的四种更新策略

策略一: Cache Aside(旁路缓存) - 最常用!

读操作:

java 复制代码
public Product getProduct(Long id) {
    // 1. 先读缓存
    String cacheKey = "product:" + id;
    String productJson = redisTemplate.opsForValue().get(cacheKey);
    
    if (productJson != null) {
        // 2. 缓存命中,直接返回
        return JSON.parseObject(productJson, Product.class);
    }
    
    // 3. 缓存未命中,查数据库
    Product product = productMapper.selectById(id);
    
    if (product != null) {
        // 4. 写入缓存
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
    }
    
    return product;
}

写操作(重点!):

java 复制代码
@Transactional
public void updateProduct(Product product) {
    // 1. 先更新数据库
    productMapper.updateById(product);
    
    // 2. 再删除缓存(不是更新缓存!)
    String cacheKey = "product:" + product.getId();
    redisTemplate.delete(cacheKey);
}

为什么是删除缓存而不是更新缓存?

假如是更新缓存:

复制代码
线程A: UPDATE DB(price=100)
线程B: UPDATE DB(price=200)
线程B: SET Redis(price=200)
线程A: SET Redis(price=100)  ← Redis是旧数据!

删除缓存的话:

复制代码
线程A: UPDATE DB(price=100)
线程A: DEL Redis
线程B: UPDATE DB(price=200)
线程B: DEL Redis
线程C: GET → 缓存未命中 → 查DB → SET Redis(price=200) ✓

还有一个问题:为什么是先更新DB,再删除缓存?

如果先删缓存:

复制代码
线程A: DEL Redis
线程B: GET → 缓存未命中 → 查DB(旧数据) → SET Redis(旧数据)
线程A: UPDATE DB(新数据)
→ Redis是旧数据,DB是新数据,不一致!

先更新DB:

复制代码
线程A: UPDATE DB(新数据)
线程A: DEL Redis
线程B: GET → 缓存未命中 → 查DB(新数据) → SET Redis(新数据) ✓

但是!先更新DB也有极端情况:

复制代码
线程A: 查DB(旧数据)
线程B: UPDATE DB(新数据)
线程B: DEL Redis
线程A: SET Redis(旧数据)  ← 又不一致了!

不过这种情况概率极低,因为:

  • 写数据库比读数据库慢
  • 线程A查完DB还要SET Redis,这段时间足够线程B删除缓存了

解决方案:延时双删

java 复制代码
@Transactional
public void updateProduct(Product product) {
    // 1. 先删一次缓存
    redisTemplate.delete(cacheKey);
    
    // 2. 更新数据库
    productMapper.updateById(product);
    
    // 3. 延时后再删一次缓存
    threadPool.schedule(() -> {
        redisTemplate.delete(cacheKey);
    }, 500, TimeUnit.MILLISECONDS);
}

策略二: Read/Write Through(读写穿透)

这种策略是应用只和缓存打交道,缓存负责和数据库同步。

java 复制代码
// 伪代码,一般用现成的框架实现
Cache.get(key) {
    if (cache.has(key)) {
        return cache.get(key);
    } else {
        data = db.query(key);
        cache.set(key, data);
        return data;
    }
}

Cache.set(key, value) {
    db.update(key, value);
    cache.set(key, value);
}

优点 : 应用逻辑简单 缺点: 需要缓存框架支持,实现复杂

策略三: Write Behind(异步写回)

原理: 更新数据时只更新缓存,缓存异步批量写回数据库。

java 复制代码
// 伪代码
Cache.set(key, value) {
    cache.set(key, value);
    queue.add(key, value); // 加入异步队列
}

// 后台线程定期批量刷入DB
scheduler.schedule(() -> {
    List<KV> batch = queue.poll(100);
    db.batchUpdate(batch);
}, 1, TimeUnit.SECONDS);

优点 : 写性能极高 缺点:

  • 可能丢数据(还没写DB就挂了)
  • 实现复杂

策略四: Refresh Ahead(提前刷新)

原理: 预测哪些数据快过期,提前异步刷新。

适合读多写少的场景。

二、我的项目经验

1. 电商商品缓存

java 复制代码
// 读:Cache Aside
public Product getProduct(Long id) {
    // 先读Redis,未命中查DB并写Redis
}

// 写:先更新DB,再删除缓存
@Transactional
public void updateProduct(Product product) {
    productMapper.updateById(product);
    redisTemplate.delete("product:" + product.getId());
    
    // 如果是热点商品,延时双删
    if (isHotProduct(product.getId())) {
        threadPool.schedule(() -> {
            redisTemplate.delete("product:" + product.getId());
        }, 500, TimeUnit.MILLISECONDS);
    }
}

2. 订单状态缓存

订单状态一致性要求高,用了MQ保证:

java 复制代码
@Transactional
public void updateOrderStatus(Long orderId, Integer status) {
    // 1. 更新数据库
    orderMapper.updateStatus(orderId, status);
    
    // 2. 发送MQ消息
    mqProducer.send("order.update", orderId);
    
    // MQ消费者删除缓存
    @RabbitListener(queues = "order.update")
    public void handleOrderUpdate(Long orderId) {
        redisTemplate.delete("order:" + orderId);
    }
}

好处: 即使删除缓存失败,MQ会重试,最终一定会删除成功。

3. 库存缓存

库存扣减要求强一致,用了分布式锁:

java 复制代码
public boolean deductStock(Long productId, Integer count) {
    RLock lock = redissonClient.getLock("lock:stock:" + productId);
    lock.lock();
    
    try {
        // 1. 查Redis库存
        Integer stock = redisTemplate.opsForValue().get("stock:" + productId);
        
        if (stock == null || stock < count) {
            return false;
        }
        
        // 2. 扣减Redis库存
        redisTemplate.opsForValue().decrement("stock:" + productId, count);
        
        // 3. 扣减DB库存
        int rows = productMapper.deductStock(productId, count);
        
        if (rows == 0) {
            // DB扣减失败,回滚Redis
            redisTemplate.opsForValue().increment("stock:" + productId, count);
            return false;
        }
        
        return true;
    } finally {
        lock.unlock();
    }
}

三、实际生产的选择

场景 一致性要求 方案
商品信息 弱,允许短暂不一致 Cache Aside
用户余额 强,不能出错 不用缓存,直接查DB + 分布式锁
订单状态 中等 Cache Aside + MQ保证删除
库存 分布式锁 + 先扣DB再扣Redis
文章阅读数 异步写回

💡 总结:

  • 99%的场景用Cache Aside: 先更新DB,再删除缓存
  • 极端情况用延时双删
  • 强一致性用分布式锁
  • 最终一致性用MQ

💡 面试加分项: 说"我们项目的商品缓存用Cache Aside,但是库存缓存用分布式锁保证强一致"。这样体现你理解不同场景要用不同方案。


📌 二、分布式锁篇

2.1 如何用Redis实现分布式锁?

✅ 正确回答思路:

分布式锁是面试的高频考点,也是实际工作中常用的功能。我从基础实现到高级方案详细说明:

一、最简单的实现(有问题!)

java 复制代码
// ❌ 错误示范
public boolean lock(String key) {
    // SETNX: SET if Not eXists
    Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1");
    return result != null && result;
}

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

问题1: 没有过期时间,如果程序崩溃,锁永远不会释放,死锁!

二、加上过期时间(还有问题!)

java 复制代码
public boolean lock(String key) {
    Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1");
    if (result != null && result) {
        // 设置10秒过期
        redisTemplate.expire(key, 10, TimeUnit.SECONDS);
        return true;
    }
    return false;
}

问题2: SETNX和EXPIRE不是原子操作,如果SETNX成功后程序崩溃,过期时间没设置上,还是死锁!

三、原子操作(还有问题!)

java 复制代码
public boolean lock(String key) {
    // SET key value EX 10 NX
    // EX 10: 10秒过期
    // NX: Not eXists才设置
    Boolean result = redisTemplate.opsForValue()
        .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return result != null && result;
}

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

问题3: 可能删除别人的锁!

复制代码
线程A: 获取锁,处理业务(耗时12秒)
10秒后: 锁自动过期
线程B: 获取到锁
线程A: 业务处理完,删除锁 ← 删掉了线程B的锁!

四、正确的实现

java 复制代码
public class RedisLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 加锁
     * @param key 锁的key
     * @param value 锁的value,用UUID保证唯一性
     * @param expireTime 过期时间
     */
    public boolean lock(String key, String value, long expireTime) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
        return result != null && result;
    }
    
    /**
     * 释放锁(Lua脚本保证原子性)
     */
    public boolean unlock(String key, String value) {
        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 == 1;
    }
}

// 使用
public void doSomething() {
    String lockKey = "lock:order:1001";
    String lockValue = UUID.randomUUID().toString(); // 唯一标识
    
    try {
        // 1. 加锁
        boolean locked = redisLock.lock(lockKey, lockValue, 10);
        if (!locked) {
            throw new RuntimeException("获取锁失败");
        }
        
        // 2. 执行业务
        processOrder();
        
    } finally {
        // 3. 释放锁(只释放自己的锁)
        redisLock.unlock(lockKey, lockValue);
    }
}

为什么用Lua脚本?

Lua脚本在Redis中是原子执行的,保证了"判断是否是自己的锁"和"删除锁"这两个操作的原子性。

五、还有问题:锁续期

复制代码
业务处理时间不确定,可能超过10秒怎么办?
→ 锁自动过期,其他线程获取到锁,产生并发问题!

解决方案:看门狗(Watchdog)机制

Redisson框架实现了自动续期:

java 复制代码
@Autowired
private RedissonClient redissonClient;

public void doSomething() {
    RLock lock = redissonClient.getLock("lock:order:1001");
    
    try {
        // 1. 加锁
        lock.lock(); // 默认30秒过期
        // 或者
        lock.lock(10, TimeUnit.SECONDS); // 指定10秒过期
        
        // 2. 业务处理
        // Redisson会自动续期!每10秒(leaseTime/3)续期一次
        processOrder();
        
    } finally {
        // 3. 释放锁
        lock.unlock();
    }
}

Watchdog原理:

复制代码
1. 加锁成功,设置30秒过期
2. 启动定时任务,每10秒检查一次
3. 如果线程还持有锁,就续期到30秒
4. 直到unlock或者线程挂了

六、Redisson分布式锁的完整用法

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 1. 普通锁
     */
    public void createOrder() {
        RLock lock = redissonClient.getLock("lock:order");
        lock.lock(10, TimeUnit.SECONDS);
        try {
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 2. 尝试加锁(不阻塞)
     */
    public void tryLock() {
        RLock lock = redissonClient.getLock("lock:order");
        try {
            // 尝试加锁,最多等待3秒,锁10秒后自动释放
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (locked) {
                // 业务逻辑
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    /**
     * 3. 可重入锁
     */
    public void reentrantLock() {
        RLock lock = redissonClient.getLock("lock:order");
        lock.lock();
        try {
            // 第一次加锁
            doSomething(); // 这里面也可以加同一把锁
        } finally {
            lock.unlock();
        }
    }
    
    private void doSomething() {
        RLock lock = redissonClient.getLock("lock:order");
        lock.lock(); // 同一个线程可以重复加锁
        try {
            // ...
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 4. 公平锁(按请求顺序获取锁)
     */
    public void fairLock() {
        RLock lock = redissonClient.getFairLock("lock:order");
        lock.lock();
        try {
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 5. 联锁(MultiLock)
     * 同时锁住多个资源
     */
    public void multiLock() {
        RLock lock1 = redissonClient.getLock("lock:product:1001");
        RLock lock2 = redissonClient.getLock("lock:product:1002");
        RLock lock3 = redissonClient.getLock("lock:product:1003");
        
        RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);
        multiLock.lock();
        try {
            // 同时锁住3个商品,防止死锁
        } finally {
            multiLock.unlock();
        }
    }
    
    /**
     * 6. 红锁(RedLock)
     * 适用于Redis集群,更安全
     */
    public void redLock() {
        RLock lock1 = redissonClient.getLock("lock:order");
        RLock lock2 = redissonClient2.getLock("lock:order"); // 另一个Redis实例
        RLock lock3 = redissonClient3.getLock("lock:order"); // 第三个Redis实例
        
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        redLock.lock();
        try {
            // 至少在N/2+1个Redis实例上加锁成功才算成功
        } finally {
            redLock.unlock();
        }
    }
}

七、实际项目经验

1. 秒杀扣库存

java 复制代码
public boolean deductStock(Long productId, Integer count) {
    String lockKey = "lock:stock:" + productId;
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 尝试加锁,最多等待0秒(不等待),锁5秒
        boolean locked = lock.tryLock(0, 5, TimeUnit.SECONDS);
        if (!locked) {
            return false; // 获取锁失败,直接返回
        }
        
        // 扣减库存
        return doDeductStock(productId, count);
        
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

2. 防止重复下单

java 复制代码
@Transactional
public Long createOrder(CreateOrderDTO dto) {
    String lockKey = "lock:order:user:" + dto.getUserId();
    RLock lock = redissonClient.getLock(lockKey);
    
    lock.lock(3, TimeUnit.SECONDS);
    try {
        // 检查是否有未支付订单
        Order unpaidOrder = orderMapper.selectUnpaidOrder(dto.getUserId());
        if (unpaidOrder != null) {
            throw new BusinessException("您有未支付的订单");
        }
        
        // 创建订单
        Order order = new Order();
        // ... 设置订单信息
        orderMapper.insert(order);
        
        return order.getId();
    } finally {
        lock.unlock();
    }
}

3. 定时任务防止重复执行

java 复制代码
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void dailyTask() {
    String lockKey = "lock:task:daily";
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 尝试加锁,不等待,锁2小时
        boolean locked = lock.tryLock(0, 120, TimeUnit.MINUTES);
        if (!locked) {
            log.info("定时任务已在其他节点执行");
            return;
        }
        
        // 执行任务
        doDailyTask();
        
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

八、分布式锁的常见问题

Q1: Redis宕机了怎么办?

A: 用RedLock,在多个独立的Redis实例上加锁,至少N/2+1个成功才算成功。或者用Zookeeper、etcd等强一致性组件。

Q2: 锁的粒度如何设计?

A:

  • 粗粒度: lock:order → 所有订单操作都串行,性能差
  • 细粒度: lock:order:userId:productId → 不同用户、不同商品并行,性能好

Q3: 加锁失败怎么处理?

A:

  • 直接返回失败(适合秒杀等场景)
  • 重试(适合一般业务)
  • 加入队列异步处理

💡 总结:

  • 生产环境推荐用Redisson,功能强大,久经考验
  • 注意锁的粒度设计,太粗影响性能,太细容易死锁
  • 一定要在finally里unlock,防止死锁
  • 重要业务可以考虑RedLock或Zookeeper

相关推荐
金銀銅鐵1 小时前
浅解 Junit 4 第四篇:类上的 @Ignore 注解
java·junit·单元测试
三水不滴1 小时前
SpringBoot + Redis 滑动窗口计数:打造高可靠接口防刷体系
spring boot·redis·后端
西门吹雪分身2 小时前
K8S之Pod生命周期
java·kubernetes·k8s
hrhcode2 小时前
【Netty】一.Netty架构设计与Reactor线程模型深度解析
java·spring boot·后端·spring·netty
亓才孓2 小时前
[Spring MVC]BindingResult
java·spring·mvc
予枫的编程笔记2 小时前
【Docker进阶篇】Docker Compose实战:Spring Boot与Redis服务名通信全解析
spring boot·redis·docker·docker compose·微服务部署·容器服务发现·容器通信
会算数的⑨2 小时前
Spring AI Alibaba 学习(三):Graph Workflow 深度解析(下篇)
java·人工智能·分布式·后端·学习·spring·saa
chilavert3182 小时前
技术演进中的开发沉思-367:锁机制(上)
java·开发语言·jvm
wei7062 小时前
Redis持久化机制详解
面试