SpringBoot 分布式共享内存
背景
最新开发一个数据平台,平台本身为分布式部署,但是各个节点都是有一份缓存,缓存存储了全量数据,每次数据更新都会需要广播请求更新每一个节点的缓存。如果某一个请求失败那么内存中的数据就会造成不一致的情况,导致请求如果分配到这个没有更新内存的节点出现的结果将是不可预期的。曾经考虑过使用redis但是由于是赋能项目,所以不能导致因为赋能项目让客户必须部署redis才可以使用。因此redis方案就此夭折。
分析
针对于缓存不一致问题,归根到底就是多节点更新的问题,如果缓存可以是单节点或者分布式的就可以解决这类的问题。
方案
可以将内嵌式分布式缓存框架嵌入到项目中,当每一个SpringBoot节点启动的时候,他们互相加入成为一个集群,集群中数据的增删改查保证一致,那么整个缓存的一致性就会得到彻底解决。
调研
我们调研了hazelcast这个项目,它的特性正好满足我们的需求。项目官方文档地址【点击进入】
满足点
- 可以分布式部署也可以嵌入式部署
- 节点是无状态的,谁最先启动谁就是master节点,之后启动的节点会成为跟随着,master宕机之后第二个启动的将会变成master节点
- 数据分布式存储对外暴露的是List、Set、Map、Queue等基本集合数据结构,方便使用
- 客户端可以支持同步写入保证数据的一致性
嵌入方式
依赖引入
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>5.1.2</version>
</dependency>
集群初始化
hazelcast可以有多种发现类型,比如组播,TCPIP。我们这里采用TCPIP形式。将节点的地址注册到ZK中。当然如果你的项目无需依赖zk可以采用组播的形式,自动发现集群。这里以zk+tcpip为例。
@Configuration
@Slf4j
public class HazelcastConfig {
private HazelcastInstance hazelcastInstance;
private CuratorFramework client;
private ServiceDiscovery<Void> discovery;
private String instanceName = null;
public String getInstanceName() {
return instanceName;
}
//服务关闭时销毁集群
@PreDestroy
public void destroy() throws IOException {
hazelcastInstance.shutdown();
discovery.close();
client.close();
}
//从配置文件中读取激活的环境,zk地址,然后从zk地址中拿到hazelcast配置的ip地址组成一个集群
@Bean
public HazelcastInstance getHazelcastInstance(@Value("${hazelcast.name:HazelcastCluster}") String hazelcastName, @Value("${zookeeper.address}") String connectStr, @Value("${spring.profiles.active}") String environment) throws Exception {
CuratorFramework client = CuratorFrameworkFactory.newClient(connectStr, new RetryOneTime(3));
client.start();
client.blockUntilConnected();
InetAddress localAddress = NetUtils.getLocalAddress();
String hostAddress = localAddress.getHostAddress();
log.info("proper init hazelcast cluster hostAddress {}, applicationName {}, environment {}", localAddress, hazelcastName, environment);
ServiceInstance<Void> instance = ServiceInstance.<Void>builder()
.name(hazelcastName)
.uriSpec(new UriSpec(hostAddress +":" + DISCOVERY_PORT))
.build();
discovery = ServiceDiscoveryBuilder.builder(Void.class)
.basePath(DISCOVERY_ROOT_DIR +"/"+hazelcastName +"/"+ environment)
.client(client)
.thisInstance(instance)
.build();
discovery.start();
TcpIpConfig tcpIpConfig = new TcpIpConfig();
List<String> instances = queryOtherInstancesInZk(hazelcastName);
log.info("get instances in zk {}", instances);
tcpIpConfig.setMembers(instances);
tcpIpConfig.setEnabled(true);
JoinConfig joinConfig = new JoinConfig();
MulticastConfig multicastConfig = new MulticastConfig();
multicastConfig.setEnabled(false);
joinConfig.setMulticastConfig(multicastConfig);
joinConfig.setTcpIpConfig(tcpIpConfig);
NetworkConfig networkConfig = new NetworkConfig();
networkConfig.setJoin(joinConfig);
networkConfig.setPort(DISCOVERY_PORT);
Config config = new Config();
JetConfig jetConfig = config.getJetConfig();
jetConfig.setEnabled(true)
.setResourceUploadEnabled(false);
config.setNetworkConfig(networkConfig);
hazelcastInstance = Hazelcast.newHazelcastInstance(config);
instanceName = hazelcastInstance.getName();
log.info("local address [{}]: cluster has members: {} ",
hazelcastInstance.getCluster().getLocalMember().getSocketAddress(),
hazelcastInstance.getCluster().getMembers().size());
return hazelcastInstance;
}
//从zk中查询地址
private List<String> queryOtherInstancesInZk(String name) throws Exception {
return discovery
.queryForInstances(name)
.stream()
.map(ServiceInstance::buildUriSpec)
.collect(toList());
}
}
使用
hazelcast集群大部分的api都会集中到HazelcastInstance
里面,这里有了上面的@Bean
注入,你可以直接使用@Autowired
进行注入属性,然后使用hazelcastInstance
调取API即可。
@Autowired
private HazelcastInstance hazelcastInstance;
写在后面的话
本人使用了Hazelcast框架集成了分布式缓存和hazelcast-jet两种,目前项目运行还是比较稳定可靠的,唯一有点遗憾的时hazelcast-jet对于SQL类型的查询,子查询内不支持limit,虽然这个问题还是给我造成了很大的困扰,但是整体来说还是很好的。