Lock4j 在多租户缓存插件中不起作用

在开发一个基于 RuoYi-Vue-Plus 构建的项目时,发现 Lock4j 的缓存锁有时候不起作用。

经测试发现,只有同时通过不同的 Application 发起同一个 key 的锁时,才会出现这个问题。虽然 key 是相同的,但是两边都可以同时获取到锁。经过排查发现虽然代码中 key 是相同的,但是最终请求到 redis 中的 key 并不相同。一个是带租户的,一个是不带租户的,但是两个应用的租户都是启用了的。

项目中的 Lock4j 使用的是 lock4j-redisson-spring-boot-starter ,是基于 RedissonClient 实现的。在启用多租户时,TenantConfig 中会自动注册一个 RedissonAutoConfigurationCustomizer 类型的 Bean,通过设置一个自定义的 nameMapper 来实现缓存数据的租户隔离。

在注册 RedissonClient Bean 时,会调用已注册的 RedissonAutoConfigurationCustomizer Bean 的 customize 方法,对 Redisson 的配置进行设置。

java 复制代码
public class RedissonAutoConfiguration {

    @Autowired(
        required = false
    )
    private List<RedissonAutoConfigurationCustomizer> redissonAutoConfigurationCustomizers;

    @Bean(
        destroyMethod = "shutdown"
    )
    @ConditionalOnMissingBean({RedissonClient.class})
    public RedissonClient redisson() throws IOException {

        // ...

        if (this.redissonAutoConfigurationCustomizers != null) {
            for(RedissonAutoConfigurationCustomizer customizer : this.redissonAutoConfigurationCustomizers) {
                customizer.customize(config);
            }
        }

        return Redisson.create(config);
    }
}

问题就出现在这里。在 RedisConfigTenantConfig 中各有一个 RedissonAutoConfigurationCustomizer 类型的 Bean 定义。上面的 List<RedissonAutoConfigurationCustomizer> 中会自动注入这两个 Bean。

两个 Bean 都可以正常注入,关键在于注入的顺序。如果没有额外的设置,默认按照 Bean 的注册顺序将其添加到 List,而默认情况下 Bean 的注册顺序有一定的不确定性 。如果 TenantConfig 中定义的 RedissonAutoConfigurationCustomizer 方法先被执行,则会导致其定义的租户专用的配置被覆盖掉了。

解决方案就是通过 @Order 注解指定 Bean 的注册顺序,值越小的优先级越高,注册顺序越靠前。

RedisConfig

java 复制代码
@Bean
@Order(1)
public RedissonAutoConfigurationCustomizer redissonCustomizer() {
    return config -> {
        ObjectMapper om = objectMapper.copy();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的。序列化时将对象全类名一起保存下来
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
        // 组合序列化 key 使用 String 内容使用通用 json 格式
        CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);
        config.setThreads(redissonProperties.getThreads())
            .setNettyThreads(redissonProperties.getNettyThreads())
            .setCodec(codec);
        RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
        if (ObjectUtil.isNotNull(singleServerConfig)) {
            // 使用单机模式
            config.useSingleServer()
                // 设置redis key前缀
                .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
                .setTimeout(singleServerConfig.getTimeout())
                .setClientName(singleServerConfig.getClientName())
                .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
                .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
                .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
                .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
        }
        // 集群配置方式 参考下方注释
        RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
        if (ObjectUtil.isNotNull(clusterServersConfig)) {
            config.useClusterServers()
                // 设置redis key前缀
                .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
                .setTimeout(clusterServersConfig.getTimeout())
                .setClientName(clusterServersConfig.getClientName())
                .setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
                .setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
                .setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
                .setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
                .setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
                .setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
                .setReadMode(clusterServersConfig.getReadMode())
                .setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
        }
        log.info("初始化 redis 配置");
    };
}

TenantConfig

java 复制代码
@Bean
@Order(2)
public RedissonAutoConfigurationCustomizer tenantRedissonCustomizer(RedissonProperties redissonProperties) {
    return config -> {
        TenantKeyPrefixHandler nameMapper = new TenantKeyPrefixHandler(redissonProperties.getKeyPrefix());
        SingleServerConfig singleServerConfig = ReflectUtils.invokeGetter(config, "singleServerConfig");
        if (ObjectUtil.isNotNull(singleServerConfig)) {
            // 使用单机模式
            // 设置多租户 redis key前缀
            singleServerConfig.setNameMapper(nameMapper);
            ReflectUtils.invokeSetter(config, "singleServerConfig", singleServerConfig);
        }
        ClusterServersConfig clusterServersConfig = ReflectUtils.invokeGetter(config, "clusterServersConfig");
        // 集群配置方式 参考下方注释
        if (ObjectUtil.isNotNull(clusterServersConfig)) {
            // 设置多租户 redis key前缀
            clusterServersConfig.setNameMapper(nameMapper);
            ReflectUtils.invokeSetter(config, "clusterServersConfig", clusterServersConfig);
        }
    };
}
相关推荐
树獭叔叔30 分钟前
10-让模型更小更聪明,学而不忘:知识蒸馏与持续学习
后端·aigc·openai
JxWang0538 分钟前
Task02:链表
后端
只会cv的前端攻城狮1 小时前
Elpis-Core — 融合 Koa 洋葱圈模型实现服务端引擎
前端·后端
codetown2 小时前
2026年Zig编程语言权威指南:从系统级底层架构到现代软件工程实践
后端·程序员
cg333 小时前
cc-connect,十分钟帮你把 claude code 连接到微信,飞书,钉钉等等平台
后端·openai
用户1427868669323 小时前
Java多态的底层真相:JVM到底怎么知道该调哪个方法?(面试高频)
后端
dkbnull3 小时前
深入理解Spring两大特性:IoC和AOP
spring boot
初次攀爬者3 小时前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
摸鱼的春哥4 小时前
惊!黑客靠AI把墨西哥政府打穿了,海量数据被黑
前端·javascript·后端
考虑考虑4 小时前
JDK25模块导入声明
java·后端·java ee