缓存同步,应该是针对集群缓存环境下,对某缓存进行修改或删除后,集群内其他服务也应该同步该缓存修改,不然便会出现由缓存数据的不一致性导致服务出现的响应数据不一致。
JetCache默认不提供缓存同步 https://github.com/alibaba/jetcache/issues/260 而在 https://github.com/alibaba/jetcache/issues/303 中JetCache作者指明了应当如何进行扩展。
此处缓存同步是指在多级缓存(本地+远程...)环境中的本地缓存。而远程缓存同步一般是通过redis机制或其他远程缓存服务自带集群机制解决。
com.alicp.jetcache.Cache的结构如图
其中com.alicp.jetcache.MultiLevelCache 代表的便是多级Cache(CacheType.BOTH)。对于定义为CacheType.LOCAL 的Cache的改动,默认认为是该服务本地的,不会影响集群的缓存。而CacheType.REMOTE则认为其一致性由远端缓存服务提供。
而CacheType.BOTH 的生成在
com.alicp.jetcache.anno.support.CacheContext::buildCache(CachedAnnoConfig cachedAnnoConfig, String area, String cacheName) : Cache;
在jetcache中,对cache的操作都会触发事件CacheEvent,事件通过CacheMonitor进行消费。因此对多级缓存的同步事件捕获方式扩展如下:
/** 配置类 */
@Configuration
@AutoConfigureBefore(JetCacheAutoConfiguration.class)// 需在JetCacheAutoConfiguration前将SpringConfigProvider进行注册
public class CacheAutoConfiguration {
@Bean
public SpringConfigProvider springConfigProvider(ExRemoteCacheMonitorChains cacheMonitor) {
return new SpringConfigProvider() {
@Override
public CacheContext newContext(GlobalCacheConfig globalCacheConfig) {
return new CacheContextWrapper(globalCacheConfig, super.newContext(globalCacheConfig), cacheMonitor);
}
};
}
}
/** CacheContext 包装类 */
public class CacheContextWrapper extends CacheContext {
private final CacheContext cacheContext;
private final CacheMonitor cacheMonitor;
public CacheContextWrapper(GlobalCacheConfig globalCacheConfig,CacheContext cacheContext, CacheMonitor cacheMonitor) {
super(globalCacheConfig);
this.cacheContext = cacheContext;
this.cacheMonitor = cacheMonitor;
}
@Override
public synchronized void shutdown() {
cacheContext.shutdown();
}
@Override
public void setCacheManager(SimpleCacheManager cacheManager) {
cacheContext.setCacheManager(cacheManager);
}
@Override
public synchronized void init() {
cacheContext.init();
}
@Override
public Cache getCache(String cacheName) {
return cacheContext.getCache(cacheName);
}
@Override
public Cache getCache(String area, String cacheName) {
return cacheContext.getCache(area, cacheName);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Cache __createOrGetCache(CachedAnnoConfig cachedAnnoConfig, String area, String cacheName) {
Cache c = cacheContext.__createOrGetCache(cachedAnnoConfig, area, cacheName);
if (cachedAnnoConfig.getCacheType() == CacheType.BOTH) {
c.config().getMonitors().add(cacheMonitor);
}
return c;
}
@Override
public CacheInvokeContext createCacheInvokeContext(ConfigMap configMap) {
return cacheContext.createCacheInvokeContext(configMap);
}
}
/**
* remote cache 消息 Monitor chains
* @author yuyi
*/
@Component
public class ExRemoteCacheMonitorChains implements CacheMonitor, ApplicationRunner {
private final List cms = new ArrayList<>();
public void append(CacheMonitor cm) {
cms.add(cm);
}
private List getCacheMonitors() {
return cms;
}
@Override
public void afterOperation(CacheEvent event) {
getCacheMonitors().forEach(c -> c.afterOperation(event));
}
@Override
public void run(ApplicationArguments args) throws Exception {
Map cms = SpringContextUtils.getApplicationContext().getBeansOfType(CacheMonitor.class);
if (!Objs.isEmpty(cms)) {
cms.values().stream().filter(q -> q != this).forEach(this::append);
log.info("init JetCache EX CacheMonitor {}.", JSONObject.toJSONString(cms.keySet()));
}
}
}
对多级缓存的同步,我采用的RabbitMQ进行同步。这里使用过redis 的sub/pub。但最终还是放弃,选择了MQ进行同步。放弃的主要原因是因为没有找到一个可靠的消息重发和消息广播机制。
/**
* session 同步
*
*
* 参考:https://blog.csdn.net/weixiaohuai/article/details/82825658
*
*
* @author yuyi
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq", name = { "host", "port" })
@Slf4j
public class RabbitSessionSynchronizeConfiguration {
/**
* 交换机名称
*/
public static final String FANOUT_EXCHANGE_NAME = "synchronize.SESSION";
/**
* 当前session接收服务队列名称
*/
@Value(FANOUT_EXCHANGE_NAME + "@${app.instanceId}")
public String MYSESSIONQUEUENAME = RabbitSessionSynchronizeConfiguration.class.getName() + "@"
+ UUID.randomUUID().toString();
/**
* 创建广播形式的交换机
*
* @return 交换机实例
*/
@Bean
public FanoutExchange fanoutExchange() {
log.info("【交换机实例{}创建成功】", FANOUT_EXCHANGE_NAME);
return new FanoutExchange(FANOUT_EXCHANGE_NAME);
}
/**
* 测试队列一
*
* @return 队列实例
*/
@Bean
public Queue sessionQueue() {
log.info("【队列{}实例创建成功】", MYSESSIONQUEUENAME);
return new Queue(MYSESSIONQUEUENAME, false);
}
/**
* 绑定队列一到交换机
*
* @return 绑定对象
*/
@Bean
public Binding bingQueueToExchange() {
log.info("【绑定队列{}到交换机{}成功】", MYSESSIONQUEUENAME, FANOUT_EXCHANGE_NAME);
return BindingBuilder.bind(sessionQueue()).to(fanoutExchange());
}
@Bean
public SimpleMessageListenerContainer messageContainer(SessionSynchronizeReceiver SessionReceiver,
ConnectionFactory connectionFactory) {
// 加载处理消息A的队列
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
// 设置接收多个队列里面的消息,这里设置接收队列A
// 假如想一个消费者处理多个队列里面的信息可以如下设置:
// container.setQueues(queueA(),queueB(),queueC());
container.setQueues(sessionQueue());
container.setExposeListenerChannel(true);
// 设置最大的并发的消费者数量
container.setMaxConcurrentConsumers(10);
// 最小的并发消费者的数量
container.setConcurrentConsumers(1);
// 设置确认模式手工确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener(SessionReceiver);
return container;
}
@Autowired
private SessionEngine sessionEngine;
@Autowired
private RabbitTemplate rabbitTemplate;
private ProtostuffSerializeUtil protostuffSerializeUtil = new ProtostuffSerializeUtil();
/** 接受 */
@Component
public class SessionSynchronizeReceiver implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, com.rabbitmq.client.Channel channel) throws Exception {
log.debug("receiver {},channel :{}", message, channel);
try {
String str = new String(message.getBody(), message.getMessageProperties().getContentEncoding());
SimpleEntry sim = protostuffSerializeUtil.deserializeByStr(str);
log.debug("sim k:{} v :{}", sim.getEntity1(), sim.getEntity2());
if (StringUtils.equals(MYSESSIONQUEUENAME, sim.getEntity1())) {
return;
}
log.info("synchronize session {}", Objs.toString(sim.getEntity2()));
sessionEngine.local().remove(Objs.toString(sim.getEntity2()));
} catch (Exception e) {
log.error("", e);
} finally {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}
public static interface SessionSender {
public void sendMessage(Object message);
}
/** 发送 */
@Component
public class SessionSynchronizeSender implements SessionSender, CacheMonitor {
/**
* 发送消息
*
* @param message 消息内容 说明: routingKey可以指定也可以不指定,这里我们给一个空字符串""
*/
@Override
public void sendMessage(Object message) {
log.debug("push {}", message);
try {
rabbitTemplate.convertAndSend(FANOUT_EXCHANGE_NAME, "",
protostuffSerializeUtil.serializeToStr(new SimpleEntry<>(MYSESSIONQUEUENAME, message)));
} catch (Exception e) {
log.error("", e);
}
}
/**
* 由本地remote cache 修改操作触发
*/
@Override
public void afterOperation(CacheEvent event) {
try {
if (event instanceof CachePutEvent) {
CachePutEvent pe = (CachePutEvent) event;
sendMessage((String) pe.getKey());
} else if (event instanceof CacheRemoveEvent) {
CacheRemoveEvent re = (CacheRemoveEvent) event;
sendMessage((String) re.getKey());
}
} catch (Exception e) {
log.error("", e);
}
}
}
}