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);
        }
    };
}
相关推荐
阿丰资源2 小时前
基于Spring Boot的电影城管理系统(直接运行)
java·spring boot·后端
IT_陈寒2 小时前
SpringBoot自动配置的坑差点让我加班到天亮
前端·人工智能·后端
消失的旧时光-19433 小时前
Spring Boot 工程化进阶:统一返回 + 全局异常 + AOP 通用工具包
java·spring boot·后端·aop·自定义注解
追风筝的人er3 小时前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
前端·vue.js·后端
StockTV4 小时前
印度股票实时数据 NSE和BSE的实时行情、K 线及指数数据
java·开发语言·spring boot·python
金銀銅鐵4 小时前
[git] 如何丢弃对一个文件的改动?
git·后端
橘子海全栈攻城狮5 小时前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
敖正炀5 小时前
反模式与排查宝典:Spring Boot 自动配置与核心机制的常见陷阱
spring boot
smallyoung5 小时前
具有反思能力的 Agentic RAG 实战:用 LangChain4j 实现 CRAG 纠错检索
人工智能·后端