点击蓝字关注我呗~
alpha可以通过扩展部署多个实例实现高可用部署,并且通过在节点上设置启动参数↓
alpha.event.scanner.enabled=false
关闭一些节点后台定时执行的事务扫描,避免多个节点同时扫描事物表可能导致的性能问题。但是当启动事务扫描的进程宕机后会导致没有进程进行事务扫描,从而无法进行超时事务补偿。在0.4.0版本中,我们通过一种基于数据库表的抢占锁机制,实现alpha集群中主节点的动态选举,并让事务扫描方法只在主节点上执行。当主节点宕机后其他节点通过抢占的方式选出一个新的主节点,本文将介绍在0.4.0版本相关的代码实现。
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/04ab2425a967f7ca48c52f240679e695.gif)
在0.4.0版本中开启动态主节点集群模式支持只需要增加启动参数 alpha.cluster.master.enabled=true
启动两个节点
java -jar alpha-server-0.4.0-snapshot-exec.jar \ --server.port=8090 \ --alpha.server.port=8080 \ --spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \ --spring.datasource.username=saga-user \ --spring.datasource.password=saga-password \ --alpha.cluster.master.enabled=true
--server.port=8090 \
--alpha.server.port=8080 \
--spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \
--spring.datasource.username=saga-user \
--spring.datasource.password=saga-password \
--alpha.cluster.master.enabled=true
java -jar alpha-server-0.4.0-snapshot-exec.jar \ --server.port=8091 \ --alpha.server.port=8081 \ --spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \ --spring.datasource.username=saga-user \ --spring.datasource.password=saga-password \ --alpha.cluster.master.enabled=true
--server.port=8091 \
--alpha.server.port=8081 \
--spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \
--spring.datasource.username=saga-user \
--spring.datasource.password=saga-password \
--alpha.cluster.master.enabled=true
节点类型信息查看
在日志中看到 master node
则表示这个进程是主节点
01:31:07.032 [pool-3-thread-1] info org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - master node 31:07.032 [pool-3-thread-1] info org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - master node
在日志中看到 slave node
则表示这个进程是从节点
01:31:31.059 [pool-3-thread-1] info org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - slave node31:31.059 [pool-3-thread-1] info org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - slave node
节点切换
当主节点进程宕后,其他从节点会采用抢占的产生一个新的主节点
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/04ab2425a967f7ca48c52f240679e695.gif)
事务扫描是通过 eventscanner.java
实现的,并且在 alphaconfig.java
中进行初始化,可以看到在 new eventscanner
代码执行前进行了eventscannerenabled判断,这个参数就是通过alpha.event.scanner.enabled
指定的(默认是true),然后传入了nodestatus
对象,这个对象就记录着这个节点的状态(主节点或者从节点),后边会讲解 nodestatus
是如何构造的。
@bean
txconsistentservice txconsistentservice(
@value("${alpha.event.pollinginterval:500}") int eventpollinginterval,
@value("${alpha.event.scanner.enabled:true}") boolean eventscannerenabled,
scheduledexecutorservice scheduler,
txeventrepository eventrepository,
commandrepository commandrepository,
txtimeoutrepository timeoutrepository,
omegacallback omegacallback) {
if (eventscannerenabled) {
new eventscanner(scheduler,
eventrepository, commandrepository, timeoutrepository,
omegacallback, eventpollinginterval, nodestatus).run();
log.info("starting the eventscanner.");
}
txconsistentservice consistentservice = new txconsistentservice(eventrepository);
return consistentservice;
}
eventscanner.java
的 pollevents 方法进行定时事务扫描,并使用 nodestatus.ismaster()
判断自己是否是主节点,只有主节点才允许执行。看到这里大家应该知道 nodestatus.ismaster()
是我们判断主节点的关键对象,那么 nodestatus.java
是如何被创建并初始化的呢?
private void pollEvents() { scheduler.scheduleWithFixedDelay( () -> { // only pull the events when working in the master mode if(nodeStatus.isMaster()){ updateTimeoutStatus(); findTimeoutEvents(); abortTimeoutEvents(); saveUncompensatedEventsToCommands(); compensate(); updateCompensatedCommands(); deleteDuplicateSagaEndedEvents(); updateTransactionStatus(); } }, 0, eventPollingInterval, MILLISECONDS);}
scheduler.scheduleWithFixedDelay(
() -> {
// only pull the events when working in the master mode
if(nodeStatus.isMaster()){
updateTimeoutStatus();
findTimeoutEvents();
abortTimeoutEvents();
saveUncompensatedEventsToCommands();
compensate();
updateCompensatedCommands();
deleteDuplicateSagaEndedEvents();
updateTransactionStatus();
}
},
0,
eventPollingInterval,
MILLISECONDS);
}
我们在 alphaconfig.java
中通过以下方式创建实例,以确保无论您是否指定了 alpha.cluster.master.enabled
参数事务扫描都可以正常工作,在这里可以看到当我们开启了集群模式后节点刚启动的时候状态是slave,下面会说明状态是如何切换到master的。
@bean
nodestatus nodestatus (){
if(masterenabled){
return new nodestatus(nodestatus.typeenum.slave);
}else{
return new nodestatus(nodestatus.typeenum.master);
}
}
@autowired
nodestatus nodestatus;
控制节点状态切换的是 clusterlockservice.java
,这个服务会定时执行锁抢占,抢占成功后设置本节点为master,否则为slave
@AutowiredLockProvider lockProvider;......@Scheduled(cron = "0/1 * * * * ?")public void masterCheck() {if (applicationReady) { this.locker = lockProvider.lock(this.getMasterLock()); if (this.locker.isPresent()) { if (!this.locked) { this.locked = true; nodeStatus.setTypeEnum(NodeStatus.TypeEnum.MASTER); LOG.info("Master Node"); } //Keep locked } else { if (this.locked || !lockExecuted) { locked = false; nodeStatus.setTypeEnum(NodeStatus.TypeEnum.SLAVE); LOG.info("Slave Node"); } } lockExecuted = true;}}
LockProvider lockProvider;
...
...
@Scheduled(cron = "0/1 * * * * ?")
public void masterCheck() {
if (applicationReady) {
this.locker = lockProvider.lock(this.getMasterLock());
if (this.locker.isPresent()) {
if (!this.locked) {
this.locked = true;
nodeStatus.setTypeEnum(NodeStatus.TypeEnum.MASTER);
LOG.info("Master Node");
}
//Keep locked
} else {
if (this.locked || !lockExecuted) {
locked = false;
nodeStatus.setTypeEnum(NodeStatus.TypeEnum.SLAVE);
LOG.info("Slave Node");
}
}
lockExecuted = true;
}
}
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/04ab2425a967f7ca48c52f240679e695.gif)
在前边的说明中可以看到,在 clusterlockservice.java
的 mastercheck
方法中通过 this.locker = lockprovider.lock(this.getmasterlock());
获取一个锁并判断是否锁成功。
lockprovider.java
是一个接口,目前我们提供了基于 jdbc 的实现,包结构以及类依赖关系如下:
依赖关系如下↓↓↓
lockprovider.java
接口定义了锁方法 lock
lockproviderpersistence.java
接口定义了以下三个方法,作为持久化锁的接口
initlock 创建锁,尝试进行锁定并返回锁定是否成功
updatelock 更新锁,进行再次锁定并返回是否成功(更新锁的接口设计的目的是为了非长连接锁设计,例如对于按照固定周期进行加锁的实现)
unlock 解锁,取消锁定
abstractlockprovider.java
抽象类实现了
lockprovider.java
接口的lock方法,并调用内部的lockproviderpersistence.java
接口进行锁操作
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/04ab2425a967f7ca48c52f240679e695.gif)
我喜欢和你们一起跑步,一致的步伐,一样的心跳,这感觉真好,当你掉队了,我带着大家跑
jdbc 类关系图
jdbclockpersistence.java
lockproviderpersistence.java
接口实现,用来实现对数据库表操作jdbclockprovider.java
继承抽象类
abstractlockprovider.java
,在构造函数中传入 lockproviderpersistence.java的接口实现
jdbclockpersistence`lockproviderjdbcconfiguration.java
锁的jdbc实例构造类
jpa
masterlockentityrepository.java、springmasterlockrepository.java、org.apache.servicecomb.pack.alpha.server.cluster.master.provider.jdbc.jpa.*
锁定表结构设计
create table if not exists master_lock (
servicename varchar(36) not null,
expiretime datetime not null default current_timestamp,
lockedtime datetime not null default current_timestamp,
instanceid varchar(255) not null,
primary key (servicename)
) engine=innodb default charset=utf8;
servicename 服务名,这个字段取值
${spring.application.name}
expiretime 锁定过期时间,这个字段取值 lockedtime+5s
lockedtime 最近一次锁定时间
instanceid 集群实例id,这个字段取值
${alpha.server.host}]:${alpha.server.port}
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/04ab2425a967f7ca48c52f240679e695.gif)
加锁/更新锁的过程是一个周期性重复执行的动作,步骤如下:
clusterlockservice.java
服务中会每秒调用一次lockprovider.java
接口的lock方法abstractlockprovider.java
抽象类中的 lock方法会尝试调用jdbclockpersistence.java
的 inilock方法进行加锁,加锁的sql实现定义在masterlockentityrepository.java
中如果表为空,那么插入一条记录并返回加锁成功
如果表中存在servicename字段相同的记录,则捕获异常加锁失败
@transactional @modifying @query(value = "insert into master_lock " + "(servicename, expiretime, lockedtime, instanceid) " + "values " + "(?1, ?2, ?3, ?4)", nativequery = true) int initlock( @param("servicename") string servicename, @param("expiretime") date expiretime, @param("lockedtime") date lockedtime, @param("instanceid") string instanceid);
@modifying
@query(value = "insert into master_lock "
+ "(servicename, expiretime, lockedtime, instanceid) "
+ "values "
+ "(?1, ?2, ?3, ?4)", nativequery = true)
int initlock(
@param("servicename") string servicename,
@param("expiretime") date expiretime,
@param("lockedtime") date lockedtime,
@param("instanceid") string instanceid);
如果 initlock加锁失败,则尝试调用
masterlockentityrepository.java
中的 updatelock 方法尝试更新锁
表中存在的本服务记录 instanceid 与本实例 instanceid 相同,则更新成功并返回加锁成功(这个表明上一个更新周期也是本服务更新的)
表中存在的本服务记录 expiretime 小于当前锁定时间 ,则更新成功并返回加锁成功(表示上一个锁定周期并没有实例进行锁定更新操作)
@transactional @modifying(clearautomatically = true) @query("update org.apache.servicecomb.pack.alpha.server.cluster.master.provider.jdbc.jpa.masterlock t " + "set t.expiretime = :expiretime" + ",t.lockedtime = :lockedtime " + ",t.instanceid = :instanceid " + "where t.servicename = :servicename and (t.expiretime <= :lockedtime or t.instanceid = :instanceid)") int updatelock( @param("servicename") string servicename, @param("lockedtime") date lockedtime, @param("expiretime") date expiretime, @param("instanceid") string instanceid);
@modifying(clearautomatically = true)
@query("update org.apache.servicecomb.pack.alpha.server.cluster.master.provider.jdbc.jpa.masterlock t "
+ "set t.expiretime = :expiretime"
+ ",t.lockedtime = :lockedtime "
+ ",t.instanceid = :instanceid "
+ "where t.servicename = :servicename and (t.expiretime <= :lockedtime or t.instanceid = :instanceid)")
int updatelock(
@param("servicename") string servicename,
@param("lockedtime") date lockedtime,
@param("expiretime") date expiretime,
@param("instanceid") string instanceid);
释放锁 unlock
保留接口,用于扩展其他锁定方式
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/04ab2425a967f7ca48c52f240679e695.gif)
可以通过lockprovider.java
和
lockproviderpersistence.java
接口实现其他方式的锁,例如zookeeper,redis等
注意
基于数据库表的方式需要集群中多个节点的服务器时钟同步
基于 mysql 数据库时需要配置正确的时区,例如:
servertimezone=gmt%2b8
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/54c8e064146bb5b54b856754fcf66363.gif)
未来,新功能还会不断加入,欢迎大家一起参与完善。共同打造优秀的微服务生态圈。
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/8aa3a132847f9b89ab6f9dc51e01d73e.gif)
Pack 0.4.0 的重要更新
*提供Alpha HA实现
*支持使用eureka或consul进行Alpha的服务发现
*修复事件扫描线程退出的问题
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/8aa3a132847f9b89ab6f9dc51e01d73e.gif)
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/cbf02b1a2e3506e2969f65237c4c56f3.png)
Javachaiss1.2.0新特性解读 |使用inspector功能...
Pack 0.4.0新特性解读 |集成Spring Cloud Eureka
项目地址:
https://github.com/apache/servicecomb-pack
更多信息请浏览官网:
http://servicecomb.apache.org/cn
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/73cbdf2e12c6e434996a8a900d4f5053.png)
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/05dfe7544ea36b3a49b9e12b67f35bfe.png)
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/ddb5018801523e082715b8403c6889fa.png)
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/2cf800f0ab9f3c0a24e5ecddcae433ff.gif)
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/6a7257379a8a4e1d74c816aa6c9efe9f.png)
戳“阅读原文”给Pack点个“⭐”吧