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);
        }
    };
}
相关推荐
Mahir085 小时前
Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析
java·后端·spring·缓存·面试·循环依赖·三级缓存
IT_陈寒9 小时前
Redis缓存击穿把我整不会了,原来还有这手操作
前端·人工智能·后端
kyriewen9 小时前
面试官让我查各部门工资最高的员工,我用AI三秒写出窗口函数,他愣了
后端·mysql·面试
文心快码BaiduComate9 小时前
干货|Comate Harness Engineering工程实践指南
前端·后端·程序员
光辉GuangHui9 小时前
Agent Skill 也需要测试:如何搭建 Skill 评估框架
前端·后端·llm
我是谁的程序员9 小时前
Mac 上生成 AppStoreInfo.plist 文件,App Store 上架
后端·ios
irving同学4623810 小时前
Node 后端实战:JWT 认证与生产级错误处理
前端·后端
Master_Azur10 小时前
单元测试——Junit单元测试框架
后端
用户83562907805110 小时前
使用 Python 进行 Word 邮件合并
后端
用户83562907805110 小时前
Python 操作 PowerPoint OLE 对象
后端·python