逸仙电商Seata企业级落地实践
作者 | 张嘉伟(GitHub ID:l81893521)
就职于逸仙电商交易中心;Seata Committer,加入 Seata 社区已有一年半,见证了从 Fescar 到 Seata 的变更,GA等。
你可能没有听说过逸仙电商,但是你的女朋友不可能没有听说过它。逸仙电商旗下有完美日记、小奥汀、完子心选等品牌。完美日记作为国货美妆界的黑马用了不到三年时间,达到了行业龙头企业通常需要十年以上才能达到的营收规模。2020 年正式登陆纽约证券交易所,成为第一家在美国上市的“国货美妆品牌”。在快速增长的业务下,系统流量增长速度越来越快,服务数量不断增多,调用链路错综复杂,数据不一致的问题日渐显现,为了降低人力成本和系统资源,我们选择了 Seata。
本文将会以逸仙电商的业务作为背景, 先介绍一下seata的原理, 并给大家进行线上演示, 由浅入深去介绍这款中间件, 以便读者更加容易去理解 Seata 这个中间件。
1. 问题背景
在微服务的架构下,数据不一致的产生原因
2. 业务介绍
挑选了逸仙电商一些比较简单易懂的业务作为开展背景
3. 原理分析
Seata的实现原理和故障解决以及部署方案
4. Demo演示
如何在线体验这款中间件,无需整合和下载任何代码
数据不一致的原因
在微服务的环境下,由于调用链路跨越多个应用,甚至跨越多个数据源,数据的一致性在普通情况下难以保证,导致数据不一致的原因非常多,这里列举了三个最常见的原因
- 业务异常一个服务链路调用中,如果调用的过程出现业务异常,产生异常的应用独立回滚,非异常的应用数据已经持久化到数据库。
- 网络异常调用的过程中,由于网络不稳定,导致链路中断,部分应用业务执行完成,部分应用业务未被执行。
- 服务不可用若服务不可用,无法被正常调用,也会导致问题的产生
这里挑选了逸仙电商业务体系里面一个非常通俗容易理解的调用方式,并且去掉了多余复杂的链路,方便在阅读过程中更加关注重点。
在以往如果出现数据不一致的问题,相信大多数的解决方案是这样的
- 人工补偿数据
- 定时任务检查和补偿数据
但是这两种方式的缺点也是显然意见的,一种是浪费大量的人力成本和时间,另外一种是浪费大量的系统资源去检查数据是否一致和额外的人力成本。
接下来我会根据逸仙在生产上稳定运行将近一年总结的经验并且尽可能简单的去描述Seata是如何保证数据一致的。
原理
在接触一项新技术之前,我们应该先从宏观的角度去理解它大概包含些什么。在Seata中,它大概分为以下三个角色。
- 黄色,Transaction Manager(TM),client端
- 蓝色,Resource Manager(RM),client端
- 绿色,Transaction Coordinator(TC),server端
你可以根据颜色,名字,缩写甚至客户端/服务端去区分这三者的关系,同时简单去理解它们每一个自身的职责大概是要干些什么事情,后面的讲解我也会保持一样的颜色和名字来区分它们。
Seata其中只一个核心是数据源代理,意味着在你执行一句Sql语句时,Seata会帮你在执行之前和之后做一些额外的操作,从而保证数据的一致性,并且尽可能做到无感知,让你使用起来感觉非常方便和神奇。这里首先要去理解两个知识点。
- 前置镜像(Before Image):保存数据变更前的样子
- 后置镜像(After Image):保存数据变更后的样子
- Undo Log:保存镜像
有时候新项目接入的时候,有同事会问,为什么事务不生效,如果你也遇到过同样的问题,那首先要检查一下自己的数据源是否已经代理成功。
当执行一句Sql时,Seata会尝试去获取这条/批数据变更前的内容,并保存到前置镜像中(Insert语句没有前置镜像),然后执行业务Sql,执行完后会尝试去获取这条/批数据变更后的内容,并保存到后置镜像中(Delete语句没有后置镜像),之后会进行分支事务注册,TC在收到分支事务注册请求时,会持久化这些分支事务信息和根据操作数据的主键为维度作为全局锁并持久化,可选持久化方式有
- file
- db
- redis
在收到TC返回的分支注册成功响应后,会把镜像持久化到应用所在的数据源的Undo Log表中,最后提交本地事务。
以上所有操作都会保证在同一个本地事务中,保证业务操作和Undo Log操作的原子性
一阶段
理解了单个应用的处理流程,再从一个完全的调用链路,去看Seata的处理过程,相信理解起来会简单很多,
- 首先一个使用了@GlobalTransactional的接口被调用,Seata会对其进行拦截,拦截的角色我们称之为TM,这个时候会访问TC开启一个新的全局事务,TC收到请求后会生成XID和全局事务信息并持久化,然后返回XID。
- 在每一层的调用链路中,XID都必须往下传递,然后每一层都经过之前说过的处理逻辑,直到执行完成/异常抛出。
直到目前,一阶段已经执行完成。
另外一个需要注意的问题是,如果发现事务不生效,需要检查XID是否成功往下传递
二阶段提交
如果在整个调用链路的过程,没有发生任何异常,那么二阶段提交的过程是非常简单而且非常的高效,只有两步
- TC清理全局事务对应的信息
- RM清理对应Undo Log信息
二阶段回滚
若调用过程中出现异常,会自动触发反向回滚
反向回滚表示,如果调用链路顺序为 A -> B -> C,那么回滚顺序为 C -> B -> A。
例:A=Insert,B=Update,如果回滚时不按照反向的顺序进行回滚,则有可能出现回滚时先把A删除了,再更新A,引发错误
在回滚的过程中有可能会遇到一种非常极端的情况,回滚到对应的模块时,找不到对应的Undo Log,这种情况主要发生在
- 分支事务注册成功,但是由于网络原因收不到成功的响应,Undo Log未被持久化
- 同时全局事务超时(超时时间可自由配置)触发回滚
这时候RM会持久化一个特殊的Undo Log,状态为GlobalFinished。由于这个全局事务已经回滚,需要防止网络恢复时,未持久化Undo Log的应用收到了分支注册成功的响应和持久化Undo Log,并提交本地最终引发的数据不一致。
读已提交
由于在一阶段的时候,数据已经保存到数据库并提交,所以Seata默认的隔离级别为读未提交,如果需要把隔离级别提升至读已提交则需要使用@GlobalLock标签并且在查询语句上加上for update
@GlobalLock
@Transactional
public PayMoneyDto detail(ProcessOnEventRequestDto processOnEventRequestDto) {
return baseMapper.detail(processOnEventRequestDto.getProcessInfoDto().getBusinessKey())
} @Mapper
public interface PayMoneyMapper extends BaseMapper<PayMoney> { @Select("select id, name, amount, account, has_repayment, pay_amount from pay_money m where m.business_key = #{businessKey} for update")
PayMoneyDto detail(@Param("businessKey") String businessKey);
}
这个时候Seata会对添加了for update的查询语句进行代理
如果一个全局事务1正在操作,并且未进行二阶段提交/回滚的时候,全局锁是被全局事务1锁持有的,同时另外一个全局事务2尝试去查询相同的数据,由于查询语句被代理,seata会尝试去获取这条数据的全局锁,直到获取成功/失败(重试次数达到配置值)为止。
问题
在生产上运行接近1年时间,总体来说遇到的问题不算多,解决起来也比较容易,比如以下这个问题
经过排查发现,由于Seata会使用jdbc标准接口尝试获取业务操作所对应的表结构,由于表结构改动频率较少,并且考虑到表结构变更后应用会进行重启,所以会对表结构进行缓存,如果表结构改动后不对应用进行重启,有可能引发构建镜像时出现NullPointerException。下面贴出关键代码
@Override
public TableMeta getTableMeta(final Connection connection, final String tableName, String resourceId) {
if (StringUtils.isNullOrEmpty(tableName)) {
throw new IllegalArgumentException("TableMeta cannot be fetched without tableName");
} TableMeta tmeta;
final String key = getCacheKey(connection, tableName, resourceId);
//错误关键处,尝试从缓存获取表结构
tmeta = TABLE_META_CACHE.get(key, mappingFunction -> {
try {
return fetchSchema(connection, tableName);
} catch (SQLException e) {
LOGGER.error("get table meta of the table `{}` error: {}", tableName, e.getMessage(), e);
return null;
}
}); if (tmeta == null) {
throw new ShouldNeverHappenException(String.format("[xid:%s]get table meta failed," +
" please check whether the table `%s` exists.", RootContext.getXID(), tableName));
}
return tmeta;
}
修改表结构,需要对应用进行重启,即可解决此问题,非常简单
第二个遇到的问题就是在生产运行一段时间后,发现branch_table和lock_table存在数据残留,并且根据xid查询global_table没有对应的数据,导致后续操作相同的数据行会出现获取全局锁失败,并且会每隔一段时间小量出现。这个异常隐藏的比较深,而且在开发环境和测试环境无法复现,通过跟踪源码和总结原因发现,是由于开启了Mysql主从,导致提交/回滚时,Seata通过xid查询分支事务时,数据未同步到从库,导致遗漏了一部分分支事务数据。
源码部分
@Override
public GlobalStatus commit(String xid) throws TransactionException {
//根据xid查询信息,如果开启主从,会有可能导致查询信息不完整
GlobalSession globalSession = SessionHolder.findGlobalSession(xid);
if (globalSession == null) {
return GlobalStatus.Finished;
}
globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
// just lock changeStatus boolean shouldCommit = SessionHolder.lockAndExecute(globalSession, () -> {
// Highlight: Firstly, close the session, then no more branch can be registered.
globalSession.closeAndClean();
if (globalSession.getStatus() == GlobalStatus.Begin) {
if (globalSession.canBeCommittedAsync()) {
globalSession.asyncCommit();
return false;
} else {
globalSession.changeStatus(GlobalStatus.Committing);
return true;
}
}
return false;
}); if (shouldCommit) {
boolean success = doGlobalCommit(globalSession, false);
//If successful and all remaining branches can be committed asynchronously, do async commit.
if (success && globalSession.hasBranch() && globalSession.canBeCommittedAsync()) {
globalSession.asyncCommit();
return GlobalStatus.Committed;
} else {
return globalSession.getStatus();
}
} else {
return globalSession.getStatus() == GlobalStatus.AsyncCommitting ? GlobalStatus.Committed : globalSession.getStatus();
}
}
@Override
public GlobalStatus rollback(String xid) throws TransactionException {
//根据xid查询信息,如果开启主从,会有可能导致查询信息不完整
GlobalSession globalSession = SessionHolder.findGlobalSession(xid);
if (globalSession == null) {
return GlobalStatus.Finished;
}
globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
// just lock changeStatus
boolean shouldRollBack = SessionHolder.lockAndExecute(globalSession, () -> {
globalSession.close(); // Highlight: Firstly, close the session, then no more branch can be registered.
if (globalSession.getStatus() == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Rollbacking);
return true;
}
return false;
});
if (!shouldRollBack) {
return globalSession.getStatus();
} doGlobalRollback(globalSession, false);
return globalSession.getStatus();
}
相信此问题会在支持Raft之后得到完美的解决
pr: https://github.com/seata/seata/pull/3086
有兴趣的朋友也可以尝试去review一下代码
部署-高可用
Seata和其他中间件的高可用部署方式差别不大,如图片所示,确保应用服务和TC访问相同的注册中心和配置中心,同时只需要启动多台TC,并将store.mode改为db模式即可完成高可用部署,并选择合适的注册中心和配置中心即可,目前支持的配置中心有
- nacos
- consul
- etcd3
- eureka
- redis
- sofa
- zookeeper
可选的配置中心有
- nacos
- etcd3
- consul
- apollo
- zk
部署-单节点多应用
当然也有更加灵活的部署方式,通过vgoup-mapping(事务集群),可以做到单节点多应用的隔离,比如A应用和B应用访问A-Group的两个TC,C应用和D应用访问B-Group的两个TC,E应用和F应用访问C-Group的两个TC。
部署-异地容灾
通过vgoup-mapping也可以做到异地容灾,当原有集群出现不可用时,可以通过变更配置立刻转移到备用的集群上。此处以Nacos作为注册中心举例,TC配置方式如下:
# 广州机房
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10 nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "Guangzhou"
username = ""
password = ""
}
}
# 上海机房
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10 nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "Shanghai"
username = ""
password = ""
}
}
本文为阿里云原创内容,未经允许不得转载。
逸仙电商Seata企业级落地实践的更多相关文章
- zz京东电商推荐系统实践
挺实在 今天为大家分享下京东电商推荐系统实践方面的经验,主要包括: 简介 排序模块 实时更新 召回和首轮排序 实验平台 简介 说到推荐系统,最经典的就是协同过滤,上图是一个协同过滤的例子.协同过滤主要 ...
- Java企业级电商项目架构演进之路 Tomcat集群与Redis分布式
史诗级Java/JavaWeb学习资源免费分享 欢迎关注我的微信公众号:"Java面试通关手册"(坚持原创,分享各种Java学习资源,面试题,优质文章,以及企业级Java实战项目回 ...
- Java从零到企业级电商项目实战
欢迎关注我的微信公众号:"Java面试通关手册"(坚持原创,分享各种Java学习资源,面试题,优质文章,以及企业级Java实战项目回复关键字免费领取)回复关键字:"电商项 ...
- 项目二:企业级java电商网站开发(服务端)
声明:项目源于网络,支持正版教程,学习使用,仅记录在此 项目介绍 企业级java电商网站开发(服务端),模块划分:用户管理,商品管理,商品品类管理,订单管理,订单详情管理,购物车管理,收货地址管理,支 ...
- Java生鲜电商平台-SpringCloud分布式请求跟踪系统设计与实践
Java生鲜电商平台-SpringCloud分布式请求跟踪系统设计与实践 Java生鲜电商平台微服务现状 某个服务挂了,导致上游大量报警,如何快速定位哪个服务出问题? 某个核心挂了,导致大量报错,如何 ...
- ASP.NET Core基于K8S的微服务电商案例实践--学习笔记
摘要 一个完整的电商项目微服务的实践过程,从选型.业务设计.架构设计到开发过程管理.以及上线运维的完整过程总结与剖析. 讲师介绍 产品需求介绍 纯线上商城 线上线下一体化 跨行业 跨商业模式 从0开始 ...
- Java 18套JAVA企业级大型项目实战分布式架构高并发高可用微服务电商项目实战架构
Java 开发环境:idea https://www.jianshu.com/p/7a824fea1ce7 从无到有构建大型电商微服务架构三个阶段SpringBoot+SpringCloud+Solr ...
- Golang 在电商即时通讯服务建设中的实践
马蜂窝技术原创文章,更多干货请搜索公众号:mfwtech 即时通讯(IM)功能对于电商平台来说非常重要,特别是旅游电商. 从商品复杂性来看,一个旅游商品可能会包括用户在未来一段时间的衣.食.住.行等 ...
- Java电商支付系统手把手实现(二) - 数据库表设计的最佳实践
1 数据库设计 1.1 表关系梳理 仔细思考业务关系,得到如下表关系图 1.2 用户表结构 1.3 分类表结构 id=0为根节点,分类其实是树状结构 1.4 商品表结构 注意价格字段的类型为 deci ...
- 微服务电商项目发布重大更新,打造Spring Cloud最佳实践!
Spring Cloud实战电商项目mall-swarm地址:转发+关注 私信我获取地址 系统架构图 系统架构图 项目组织结构 mall├── mall-common-- 工具类及通用代码模块├─ ...
随机推荐
- 专访冠军考拉ok|“新人问我学Blender能找到工作吗,我回复不能”
"新锐先锋,玩转未来"--首届实时染3D动画创作大赛由瑞云科技主办,英伟达.青椒云.3DCAT实时渲染云协办,戴尔科技集团.Reallusion.英迈.万生华态.D5渲染器.中视典 ...
- 您真的了解Java中的锁吗?这7种不同维度下的锁知道吗?
写在开头 在上几篇博文中,我们聊到过volatile关键字,用它修饰变量可以保证可见性与有序性,但它并不是锁,在使用时并不会阻塞线程,且不保证原子性,属于一种轻量级.高效的同步方式,因此,如果我们的使 ...
- 靶场搭建----phpstudy2018安装及注意问题
安装 官网下载: https://www.xp.cn/download.html 新人推荐2018 版本phpstudy 介绍 系统服务------开机自启 非服务模式------开机不自启 搭建好环 ...
- Java解析json数据(fastjson2)
Json数据 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式.它以易于阅读和编写的方式来表示结构化数据,常用于在不同系统之间进行数据交互和传输. JSON使 ...
- 软件发布版本号命名风格(GUN)
GUN风格: (1)产品初版时,版本号可以为0.1或0.1.0,也可以为1.0或1.0.0: (2)当产品进行了局部修改或bug修正时,主版本号和子版本号都不变,修正版本号+1: (3)当产品在原有的 ...
- xpath解析爱奇艺电影网页数据
1 url='https://list.iqiyi.com/www/1/-------------11-1-1-iqiyi--.html' 2 headers={ 3 'User-Agent':'Mo ...
- #轮廓线dp,博弈论#洛谷 4363 [九省联考 2018] 一双木棋 chess
题目传送门 分析 菲菲想让答案尽量大,牛牛想让答案尽量小. 很天真的一种想法就是设 \(dp[i][j]\) 表示现在选择 \((i,j)\) 的答案. 但是这样有一个弊端就是并不知道其它位置怎么选择 ...
- OpenHarmony轻松玩转GIF数据渲染
OpenAtom OpenHarmony(以下简称"OpenHarmony")提供了Image组件支持GIF动图的播放,但是缺乏扩展能力,不支持播放控制等.今天介绍一款三方库--o ...
- 李俊刚:我是如何在OpenHarmony完成ap6275s WiFi驱动的HDF适配工作的?
编者按:在 OpenHarmony 生态发展过程中,涌现了大批优秀的代码贡献者,本专题旨在表彰贡献.分享经验,文中内容来自嘉宾访谈,不代表 OpenHarmony 工作委员会观点. 李俊刚 深圳开鸿数 ...
- Java 枚举(Enums)解析:提高代码可读性与易维护性
接口 在 Java 中,实现抽象的另一种方式是使用接口. 接口定义 接口是一个完全抽象的类,用于将具有空方法体的相关方法分组: // 接口 interface Animal { public void ...