基于Redis锁实现Spring Cache多实例同步的锁机制实现咨询
基于Redis锁实现Spring Cache多实例同步的锁机制实现咨询
嘿,这个场景我太熟了——多实例部署下用定时任务更新Redis缓存,就怕多个实例同时跑更新逻辑浪费资源,用Redis分布式锁来做同步完全是正确的思路!既然你已经用了Lettuce作为Redis客户端,那咱们就从Spring生态里的现有工具出发,给你两种靠谱的实现方式:
方式一:手动基于Lettuce API实现(灵活可控)
Spring Cache用Lettuce的话,其实Spring已经帮你配置好了LettuceConnectionFactory和StringRedisTemplate,直接注入就能用,不用自己手动管理连接,避免资源泄漏。
核心思路
Redis锁的关键是原子性加锁+防死锁+安全释放:
- 用
SET key value NX EX seconds的原子操作加锁:NX表示只有key不存在时才设置,EX是自动过期时间(防止实例挂了导致死锁) - 锁的value用唯一标识(比如UUID+实例ID),释放锁时先校验value是不是自己的,再删除(用Lua脚本保证原子性,避免误删别人的锁)
代码示例
首先注入StringRedisTemplate:
@Autowired private StringRedisTemplate stringRedisTemplate;
然后封装锁的工具方法:
// 加锁方法 private boolean acquireLock(String lockKey, String lockValue, long expireSeconds) { return Boolean.TRUE.equals(stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> { LettuceConnection lettuceConn = (LettuceConnection) connection; return lettuceConn.sync() .set(lockKey, lockValue, SetArgs.Builder.nx().ex(expireSeconds)); })); } // 释放锁方法(用Lua脚本保证原子性) private boolean releaseLock(String lockKey, String lockValue) { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; return Boolean.TRUE.equals(stringRedisTemplate.execute( new DefaultRedisScript<>(luaScript, Boolean.class), Collections.singletonList(lockKey), lockValue )); }
最后在你的定时任务里用:
@Scheduled(fixedRate = 3600000) // 比如一小时一次 public void updateCache() { // 1. 先检查缓存是否过期(你的逻辑) boolean isCacheExpired = checkIfCacheExpired(); if (!isCacheExpired) { return; } String lockKey = "cache-update-lock"; String lockValue = UUID.randomUUID().toString() + "-" + System.getenv("INSTANCE_ID"); // 唯一标识 long lockExpireSeconds = 600; // 锁过期时间,要比你的更新任务最长执行时间长 try { if (acquireLock(lockKey, lockValue, lockExpireSeconds)) { // 2. 抢到锁,执行缓存更新逻辑 refreshRedisCache(); } else { // 3. 没抢到锁,直接跳过 log.info("Another instance is updating cache, skip this time"); } } finally { // 4. 释放锁(只有自己的锁才释放) releaseLock(lockKey, lockValue); } }
注意事项
- 锁的过期时间一定要设置得比缓存更新任务的最长执行时间长,防止任务还没做完锁就过期了,导致多个实例同时更新
- 如果你的更新任务耗时不稳定,可以加个“锁续约”逻辑:比如开个后台线程,每隔一段时间(比如过期时间的1/3)检查锁还在不在,如果在就续期
方式二:用Spring Data Redis的RedisLockRegistry(省心封装)
如果你不想自己写Lua脚本和原子操作逻辑,Spring Data Redis已经给你封装好了RedisLockRegistry,它内部已经处理了原子加锁、自动续约、安全释放这些细节,直接用就行。
配置RedisLockRegistry
先在配置类里注册Bean:
@Configuration public class RedisLockConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public RedisLockRegistry redisLockRegistry() { // 第一个参数是Redis连接工厂,第二个是锁的前缀(避免和其他锁冲突) return new RedisLockRegistry(redisConnectionFactory, "cache-update-lock-group"); } }
在定时任务里使用
@Autowired private RedisLockRegistry redisLockRegistry; @Scheduled(fixedRate = 3600000) public void updateCache() { if (!checkIfCacheExpired()) { return; } Lock lock = redisLockRegistry.obtain("cache-update-lock"); try { if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试1秒内获取锁 // 抢到锁,更新缓存 refreshRedisCache(); } else { log.info("Other instance is updating cache, skip"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Failed to acquire lock", e); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
这种方式的好处是不用自己处理底层的Redis命令和Lua脚本,Spring已经帮你封装好了,而且默认会自动续约锁(只要持有锁的实例还活着,锁就不会过期),非常适合Spring项目。
总结
- 如果需要高度自定义锁的逻辑(比如特殊的过期策略、锁监控),选手动基于Lettuce API的实现
- 如果想快速开发、减少重复代码,直接用
RedisLockRegistry就行,省心又可靠
备注:内容来源于stack exchange,提问作者Fábio




