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);
    }
}

问题就出现在这里。在 RedisConfig 和 TenantConfig 中各有一个 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);
        }
    };
}
相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码4 天前
别再前后端各写一套表单校验了
java·后端
大勇前进4 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
vipxieliang4 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
yuzhi_liu4 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile4 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白804 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙4 天前
PHP 接口返回统一响应封装,让前后端对接更省心
后端