You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Redis锁实现Spring Cache多实例同步的锁机制实现咨询

基于Redis锁实现Spring Cache多实例同步的锁机制实现咨询

嘿,这个场景我太熟了——多实例部署下用定时任务更新Redis缓存,就怕多个实例同时跑更新逻辑浪费资源,用Redis分布式锁来做同步完全是正确的思路!既然你已经用了Lettuce作为Redis客户端,那咱们就从Spring生态里的现有工具出发,给你两种靠谱的实现方式:

方式一:手动基于Lettuce API实现(灵活可控)

Spring Cache用Lettuce的话,其实Spring已经帮你配置好了LettuceConnectionFactoryStringRedisTemplate,直接注入就能用,不用自己手动管理连接,避免资源泄漏。

核心思路

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

火山引擎 最新活动