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);
        }
    };
}
相关推荐
Dcs29 分钟前
Java 中 UnaryOperator 接口与 Lambda 表达式的应用示例
java·后端
bagadesu2 小时前
使用Docker构建Node.js应用的详细指南
java·后端
没有bug.的程序员2 小时前
Spring Cloud Gateway 性能优化与限流设计
java·spring boot·spring·nacos·性能优化·gateway·springcloud
q***71855 小时前
海康威视摄像头ISUP(原EHOME协议) 摄像头实时预览springboot 版本java实现,并可以在浏览器vue前端播放(附带源码)
java·前端·spring boot
勇哥java实战分享6 小时前
第一次用 Ollama 跑视觉模型:Qwen2.5-VL 7B 给了我一个意外惊喜
后端
码事漫谈7 小时前
从后端开发者到Agent工程师:一份系统性的学习指南
后端
码事漫谈7 小时前
后端开发如何将创新转化为专利?案例、流程与实操指南
后端
KYumii8 小时前
智慧判官-分布式编程评测平台
vue.js·spring boot·分布式·spring cloud·java-rabbitmq
user_admin_god8 小时前
企业级管理系统的站内信怎么轻量级优雅实现
java·大数据·数据库·spring boot
q***82918 小时前
Spring Boot 3.3.4 升级导致 Logback 之前回滚策略配置不兼容问题解决
java·spring boot·logback