繁体   English   中英

为多租户设置 Hazelcast 缓存

[英]Setting Hazelcast Cache for Multi-tenancy

我目前正在使用 JHipster 生成器生成真正的样板代码,其中涉及 HazelCast 作为二级缓存。 我能够使用基于 header 的租户上下文来获得多租户(每个租户的架构)。 我现在遇到的问题是 @Cacheable 注释都共享一个上下文。 如果缓存很热,我最终会得到跨模式数据。 例如,tenant1 从缓存的表中提取所有记录。 租户 2 从他们的表中提取相同的项目,读取缓存,并且它永远不会进入实际的租户数据库。 一个简单的解决方法是一起禁用缓存,但我不想这样做。 我一生都无法弄清楚如何让 hazelcast 了解租户上下文 - 缺少文档。 其他一些人已经使用自定义名称解析器解决了这个问题,但它似乎不像我希望的那样动态(即您必须提前了解所有租户)。 想法?

当前缓存配置:

@Configuration
@EnableCaching
public class CacheConfiguration implements DisposableBean {

    private final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);

    private final Environment env;

    private final ServerProperties serverProperties;

    private final DiscoveryClient discoveryClient;

    private Registration registration;

    public CacheConfiguration(Environment env, ServerProperties serverProperties, DiscoveryClient discoveryClient) {
        this.env = env;
        this.serverProperties = serverProperties;
        this.discoveryClient = discoveryClient;
    }

    @Autowired(required = false)
    public void setRegistration(Registration registration) {
        this.registration = registration;
    }

    @Override
    public void destroy() throws Exception {
        log.info("Closing Cache Manager");
        Hazelcast.shutdownAll();
    }

    @Bean
    public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
        log.debug("Starting HazelcastCacheManager");
        return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance);
    }

    @Bean
    public HazelcastInstance hazelcastInstance(JHipsterProperties jHipsterProperties) {
        log.debug("Configuring Hazelcast");
        HazelcastInstance hazelCastInstance = Hazelcast.getHazelcastInstanceByName("SampleApp");
        if (hazelCastInstance != null) {
            log.debug("Hazelcast already initialized");
            return hazelCastInstance;
        }
        Config config = new Config();
        config.setInstanceName("SampleApp");
        config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
        if (this.registration == null) {
            log.warn("No discovery service is set up, Hazelcast cannot create a cluster.");
        } else {
            // The serviceId is by default the application's name,
            // see the "spring.application.name" standard Spring property
            String serviceId = registration.getServiceId();
            log.debug("Configuring Hazelcast clustering for instanceId: {}", serviceId);
            // In development, everything goes through 127.0.0.1, with a different port
            if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
                log.debug("Application is running with the \"dev\" profile, Hazelcast " +
                          "cluster will only work with localhost instances");

                System.setProperty("hazelcast.local.localAddress", "127.0.0.1");
                config.getNetworkConfig().setPort(serverProperties.getPort() + 5701);
                config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
                for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
                    String clusterMember = "127.0.0.1:" + (instance.getPort() + 5701);
                    log.debug("Adding Hazelcast (dev) cluster member {}", clusterMember);
                    config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(clusterMember);
                }
            } else { // Production configuration, one host per instance all using port 5701
                config.getNetworkConfig().setPort(5701);
                config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
                for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
                    String clusterMember = instance.getHost() + ":5701";
                    log.debug("Adding Hazelcast (prod) cluster member {}", clusterMember);
                    config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(clusterMember);
                }
            }
        }
        config.getMapConfigs().put("default", initializeDefaultMapConfig(jHipsterProperties));

        // Full reference is available at: http://docs.hazelcast.org/docs/management-center/3.9/manual/html/Deploying_and_Starting.html
        config.setManagementCenterConfig(initializeDefaultManagementCenterConfig(jHipsterProperties));
        config.getMapConfigs().put("com.test.sampleapp.domain.*", initializeDomainMapConfig(jHipsterProperties));
        return Hazelcast.newHazelcastInstance(config);
    }

    private ManagementCenterConfig initializeDefaultManagementCenterConfig(JHipsterProperties jHipsterProperties) {
        ManagementCenterConfig managementCenterConfig = new ManagementCenterConfig();
        managementCenterConfig.setEnabled(jHipsterProperties.getCache().getHazelcast().getManagementCenter().isEnabled());
        managementCenterConfig.setUrl(jHipsterProperties.getCache().getHazelcast().getManagementCenter().getUrl());
        managementCenterConfig.setUpdateInterval(jHipsterProperties.getCache().getHazelcast().getManagementCenter().getUpdateInterval());
        return managementCenterConfig;
    }

    private MapConfig initializeDefaultMapConfig(JHipsterProperties jHipsterProperties) {
        MapConfig mapConfig = new MapConfig();

        /*
        Number of backups. If 1 is set as the backup-count for example,
        then all entries of the map will be copied to another JVM for
        fail-safety. Valid numbers are 0 (no backup), 1, 2, 3.
        */
        mapConfig.setBackupCount(jHipsterProperties.getCache().getHazelcast().getBackupCount());

        /*
        Valid values are:
        NONE (no eviction),
        LRU (Least Recently Used),
        LFU (Least Frequently Used).
        NONE is the default.
        */
        mapConfig.setEvictionPolicy(EvictionPolicy.LRU);

        /*
        Maximum size of the map. When max size is reached,
        map is evicted based on the policy defined.
        Any integer between 0 and Integer.MAX_VALUE. 0 means
        Integer.MAX_VALUE. Default is 0.
        */
        mapConfig.setMaxSizeConfig(new MaxSizeConfig(0, MaxSizeConfig.MaxSizePolicy.USED_HEAP_SIZE));

        return mapConfig;
    }

    private MapConfig initializeDomainMapConfig(JHipsterProperties jHipsterProperties) {
        MapConfig mapConfig = new MapConfig();
        mapConfig.setTimeToLiveSeconds(jHipsterProperties.getCache().getHazelcast().getTimeToLiveSeconds());
        return mapConfig;
    }
}

使用 cacheNames 的示例存储库...

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    String USERS_BY_LOGIN_CACHE = "usersByLogin";

    String USERS_BY_EMAIL_CACHE = "usersByEmail";

    String USERS_BY_ID_CACHE = "usersById";

    Optional<User> findOneByActivationKey(String activationKey);

    List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime);

    Optional<User> findOneByResetKey(String resetKey);

    Optional<User> findOneByEmailIgnoreCase(String email);

    Optional<User> findOneByLogin(String login);

    @EntityGraph(attributePaths = "roles")
    @Cacheable(cacheNames = USERS_BY_ID_CACHE)
    Optional<User> findOneWithRolesById(Long id);

    @EntityGraph(attributePaths = "roles")
    @Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
    Optional<User> findOneWithRolesByLogin(String login);

    @EntityGraph(attributePaths = { "roles", "roles.permissions" })
    @Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
    Optional<User> findOneWithRolesAndPermissionsByLogin(String login);

    @EntityGraph(attributePaths = "roles")
    @Cacheable(cacheNames = USERS_BY_EMAIL_CACHE)
    Optional<User> findOneWithRolesByEmail(String email);

    Page<User> findAllByLoginNot(Pageable pageable, String login);
}

我正在使用每个数据库的租户(MySQL),但是只要您在上面设置线程上下文就是我正在做的事情 - 我正在使用 Spring 引导。 我创建了一个自定义缓存密钥生成器,它结合了租户名称 + class + 和方法。 你真的可以选择任何组合。 每当我将该租户传回时,它都会提取正确的条目。 在我的 AppointmentType map 类型的 Hazelcast 命令中心中,我看到每个租户增加的条目数。

其他一些可能有用的参考资料:

在您要缓存的 class 中(我的是服务类):

@Service
public class AppointmentTypeService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AppointmentTypeService.class);

    private final AppointmentTypeRepository appointmentTypeRepository;

    @Autowired
    AppointmentTypeService(AppointmentTypeRepository appointmentTypeRepository) {
        this.appointmentTypeRepository = appointmentTypeRepository;
    }
    
    //ADD keyGenerator value. Name is the name of the bean of the class
    @Cacheable(value="appointmentType", keyGenerator = "multiTenantCacheKeyGenerator")
    public List<AppointmentType> list() {
        return this.appointmentTypeRepository.findAll();
    }

    @CacheEvict(value="appointmentType", allEntries=true)
    public Long create(AppointmentType request) {
        this.appointmentTypeRepository.saveAndFlush(request);
        return request.getAppointmentTypeId();
    }

    @CacheEvict(value="appointmentType", allEntries=true)
    public void delete(Long id) {
        this.appointmentTypeRepository.deleteById(id);
    }

    public Optional<AppointmentType> findById(Long id) {
        return this.appointmentTypeRepository.findById(id);
    }

}

创建密钥生成器 class

//setting the bean name here
@Component("multiTenantCacheKeyGenerator")
public class MultiTenantCacheKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object o, Method method, Object... os) {
        StringBuilder sb = new StringBuilder();
        sb.append(TenantContext.getCurrentTenantInstanceName()) //my tenant context class which is using local thread. I set the value in the Spring filter.
          .append("_")
          .append(o.getClass().getSimpleName())
          .append("-")
          .append(method.getName());
      }

       return sb.toString();
    }
    
}

为租户定义不同缓存键的一种方法是覆盖org.springframework.cache.CacheManager中的getCache方法,如下所示: Extended spring cache...

从 Jhipster 7.0.1开始,Hazelcast 的CacheManager在 class CacheConfiguration中定义,如下所述:

@Configuration
@EnableCaching
public class CacheConfiguration {

    //...

    @Bean
    public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
        return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance);
    }

    //...
}

要让缓存键以租户 ID 为前缀,可以使用以下代码作为起点:

@Configuration
@EnableCaching
public class CacheConfiguration {

    @Bean
    public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
        return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance){
            @Override
            public Cache getCache(String name) {
                String tenantId = TenantStorage.getTenantId();
                if (StringUtils.isNotBlank(tenantId)){
                    return super.getCache(String.format("%s:%s", tenantId, name));
                }
                return super.getCache(name);
            }
        };
    }

}

注意:在上面的代码中, TenantStorage.getTenantId()是一个 static function 应该实现并返回当前租户 ID。

考虑 OP 发布的 class :

    @Cacheable(cacheNames = "usersByLogin")
    Optional<User> findOneWithRolesByLogin(String login);

HazelCast 将使用以下缓存值:

  • 租户1 => 租户1:usersByLogin
  • 租户2 => 租户2:usersByLogin
  • null => usersByLogin

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM