为多租户设置Hazelcast缓存

3

我目前正在使用JHipster生成器来生成基础代码,并使用HazelCast作为二级缓存。我已经能够使用基于header的租户上下文实现多租户(每个租户一个schema)功能。现在遇到的问题是,所有@Cacheable注释共享一个上下文。如果缓存热了,就会出现跨模式数据。例如,租户1从他们的表中检索所有记录并将其缓存。租户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);
}
2个回答

2
我正在使用基于数据库的租户(MySQL),但只要在此上下文中设置线程,我就会执行以下操作-我正在使用Spring Boot。我创建了一个自定义缓存键生成器,它将租户名称+类+方法组合在一起。您可以选择任何组合。每当我将该租户传回时,它就会提取正确的条目。在我的AppointmentType映射类型的Hazelcast命令中心中,我看到每个租户的条目数量增加。

以下是其他可能有用的参考资料:

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

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

}

创建密钥生成器类
//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();
    }
    
}

1

为了为每个租户定义不同的缓存键,可以重写org.springframework.cache.CacheManager中的getCache方法。这里建议采用以下方式:Extended spring cache...

Jhipster 7.0.1版本中,Hazelcast的CacheManagerCacheConfiguration类中定义,如下所述:

@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() 是一个静态函数,需要实现并返回当前租户的ID。
考虑OP发布的类:
    @Cacheable(cacheNames = "usersByLogin")
    Optional<User> findOneWithRolesByLogin(String login);

以下缓存值将被HazelCast使用:
  • 租户1 => 租户1:按登录名排序的用户
  • 租户2 => 租户2:按登录名排序的用户
  • null => 按登录名排序的用户

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接