前言
ssh框架(Struts, Spring, Hibernate)在当今的开发环境中不再是主流选择,但它们依然在一些遗留系统中使用。记得上学的时候Hibernate和Mybatis还是平分秋色,现在技术选型应该没人考虑了。
本文是针对老系统集成了分布式事务,写出来有两点:
第一:如果有读者也有这样的需求,期望用手上的系统集成分布式事务,可以参考下。
第二:集成过程也有点曲折,根本没管会不会成,脑袋一热就做了,但是回头看看通过这种方式掌握了Seata,还秀了下技术,收获也算有。
准备工作:
先看看官网介绍,能看多少是多少,最终还是要实践。
Seata:
Seata(TC)下载:
下载后进入\seata-server-1.7.1\seata\bin\启动即可。windows下双击seata-server.bat
集成步骤:
引入maven依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.5.2</version>
</dependency>
依赖冲突这里就不细述了,之前将ssh项目改成了maven项目,如果还不是Maven,可以找个maven工程把这里面的jar都放进去。(后面可以分享下将ssh改成maven,纯体力活)
resource引入配置
file.conf:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.seata = "Storage"
#only support when registry.type=file, please don't set multiple addresses
Storage.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
需修改服务名称和TC地址:
registry.conf:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
application = "seata-server"
serverAddr = "localhost"
namespace = ""
username = ""
password = ""
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
password = ""
timeout = "0"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
registry.conf描述的seata的配置,注意看最后面我们使用的file.conf。当然也可以应用其他的配置。
添加数据库日志表
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3596 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
这是核心表,也是seata(AT)的关键,所有需要使用seata的服务都要加。
AT模式原理:执行sql前进行数据备份,如果失败了就用备份的sql还原数据。
添加seata数据源
在applicationContext.xml中添加seata的数据源。
<bean id="dataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
<constructor-arg ref="dataSource" />
</bean>
使用dataSourceProxy替换原先的数据源
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource">
<ref bean="dataSourceProxy" />
</property>
.......
这里也可以用多数据源,例如需要分布式事务就用dataSourceProxy数据源,不需要就用原来的数据源。这也是保守开发,将风险降低。
注册TM/RM
TM:事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。所以每个服务即是TM,也会是RM。
在applicationContext.xml中添加启动事件
<bean class="com.config.seata.SeataConfig"></bean>
在启动事件SeataConfig中注册tm/rm
import io.seata.core.context.RootContext;
import io.seata.core.model.BranchType;
import io.seata.rm.RMClient;
import io.seata.tm.TMClient;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
public class SeataConfig implements ApplicationListener<ContextRefreshedEvent> {
public void onApplicationEvent(ContextRefreshedEvent event) {
RootContext.bindBranchType(BranchType.AT);
TMClient.init("Storage","seata");
RMClient.init("Storage", "seata");
}
}
seata与file.conf中service.vgroupMapping.seata中seata一致
添加seata切点
<bean class="io.seata.spring.annotation.GlobalTransactionalInterceptor" id="globalTransactionalInterceptor"></bean>
<aop:config>
<!-- 事务切入点-->
<aop:pointcut id="pc" expression=" execution(* com..service.*.*(..))) "/>
<aop:advisor advice-ref="globalTransactionalInterceptor" pointcut-ref="pc"/>
</aop:config>
我们让事务作用于service的方法上,注意事务切入与当前项目的事务切点一致。
seata事务传递
当前系统1调用系统2时,假设使用http,需要在http调用中添加xid(全局事务id),不管什么协议,只要把这个xid传下去,所有服务都可以加入一个全局事务。
httpPost.setHeader(RootContext.KEY_XID, RootContext.getXID());
下游服务接收
public class SeataFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
//TODO seata
String xid = RootContext.getXID();
String rpcXid = request.getHeader(RootContext.KEY_XID);
if (StringUtils.isBlank(xid) && StringUtils.isNotBlank(rpcXid)) {
RootContext.bind(rpcXid);
}
chain.doFilter(req, res);
if (RootContext.inGlobalTransaction()) {
XidResource.cleanXid(request.getHeader(RootContext.KEY_XID));
}
}
@Override
public void destroy() {
}
}
注:将SeataFilter 添加到web.xml中,过滤所有请求。
业务集成
@Override
@GlobalTransactional
public Object update(HttpServletRequest request) throws Exception {
......
}
在需要开启分布式事务的地方加入@GlobalTransactional注解即可。
注意事项:
1、AT模式下,tm通知tc,所有要把所有异常都抛出来,其他rm不会通知tc。
2、http调用需要传递xid,业务集成seata要注意。
3、Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
4、回滚表无法保证支持全部sql,请具体业务详细验证。
5、所有服务都要按照步骤集成seata
总结
本章没写具体的原理,也是想着用的人少。如果能够按照步骤实现,想必也有一定的能力理清楚seata的原理。
感言
搭建微服务架构,必然会带来一系列治理问题,例如分布式事务,链路追踪,服务降级,熔断,定时任务执行等等,但万变不离其宗,再高大上的设计,都是一行一行代码体现出来的。
现手上有一个ssh的项目,开始觉得启动太麻烦,便改成了maven项目,后面集成了链路追踪,nacos和分布式事务。除此之外 拆分业务,单点登录等都可以排进去。
有兴趣的可以反馈下,我找时间分享,或者有别的想法大家一起探讨下。
技术的更新换代很快,但最重要的是拥有解决问题的创新思维和灵活的应对策略,一起加油。