11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

逸仙电商 Seata 企业级落地实践

  • 2021-04-25
  • 本文字数:5881 字

    阅读完需:约 19 分钟

逸仙电商Seata企业级落地实践

你可能没有听说过逸仙电商,但是你的女朋友不可能没有听说过它。逸仙电商旗下有完美日记、小奥汀、完子心选等品牌。完美日记作为国货美妆界的黑马用了不到三年时间,达到了行业龙头企业通常需要十年以上才能达到的营收规模。2020 年正式登陆纽约证券交易所,成为第一家在美国上市的“国货美妆品牌”。在快速增长的业务下,系统流量增长速度越来越快,服务数量不断增多,调用链路错综复杂,数据不一致的问题日渐显现,为了降低人力成本和系统资源,我们选择了 Seata。


本文将会以逸仙电商的业务作为背景, 先介绍一下 Seata 的原理, 并给大家进行线上演示, 由浅入深去介绍这款中间件, 以便读者更加容易去理解 Seata 这个中间件。

1. 问题背景

在微服务的架构下,数据不一致的产生原因。

2. 业务介绍

挑选了逸仙电商一些比较简单易懂的业务作为开展背景。

3. 原理分析

Seata 的实现原理和故障解决以及部署方案。

4. Demo 演示

如何在线体验这款中间件,无需整合和下载任何代码。

数据不一致的原因


image

在微服务的环境下,由于调用链路跨越多个应用,甚至跨越多个数据源,数据的一致性在普通情况下难以保证,导致数据不一致的原因非常多,这里列举了三个最常见的原因:


1、业务异常一个服务链路调用中,如果调用的过程出现业务异常,产生异常的应用独立回滚,非异常的应用数据已经持久化到数据库。

2、网络异常调用的过程中,由于网络不稳定,导致链路中断,部分应用业务执行完成,部分应用业务未被执行。

3、服务不可用若服务不可用,无法被正常调用,也会导致问题的产生。


image

这里挑选了逸仙电商业务体系里面一个非常通俗容易理解的调用方式,并且去掉了多余复杂的链路,方便在阅读过程中更加关注重点。


在以往如果出现数据不一致的问题,相信大多数的解决方案是这样的:

  • 人工补偿数据

  • 定时任务检查和补偿数据


但是这两种方式的缺点也是显然意见的,一种是浪费大量的人力成本和时间,另外一种是浪费大量的系统资源去检查数据是否一致和额外的人力成本。


接下来我会根据逸仙在生产上稳定运行将近一年总结的经验并且尽可能简单的去描述 Seata 是如何保证数据一致的。

原理


image

在接触一项新技术之前,我们应该先从宏观的角度去理解它大概包含些什么。在 Seata 中,它大概分为以下三个角色:

  • 黄色,Transaction Manager(TM),client 端

  • 蓝色,Resource Manager(RM),client 端

  • 绿色,Transaction Coordinator(TC),server 端


你可以根据颜色,名字,缩写甚至客户端/服务端去区分这三者的关系,同时简单去理解它们每一个自身的职责大概是要干些什么事情,后面的讲解我也会保持一样的颜色和名字来区分它们。

image

Seata 其中只一个核心是数据源代理,意味着在你执行一句 Sql 语句时,Seata 会帮你在执行之前和之后做一些额外的操作,从而保证数据的一致性,并且尽可能做到无感知,让你使用起来感觉非常方便和神奇。这里首先要去理解两个知识点。


  • 前置镜像(Before Image):保存数据变更前的样子

  • 后置镜像(After Image):保存数据变更后的样子

  • Undo Log:保存镜像


有时候新项目接入的时候,有同事会问,为什么事务不生效,如果你也遇到过同样的问题,那首先要检查一下自己的数据源是否已经代理成功。


当执行一句 Sql 时,Seata 会尝试去获取这条/批数据变更前的内容,并保存到前置镜像中(Insert 语句没有前置镜像),然后执行业务 Sql,执行完后会尝试去获取这条/批数据变更后的内容,并保存到后置镜像中(Delete 语句没有后置镜像),之后会进行分支事务注册,TC 在收到分支事务注册请求时,会持久化这些分支事务信息和根据操作数据的主键为维度作为全局锁并持久化,可选持久化方式有:


  • file

  • db

  • redis


在收到 TC 返回的分支注册成功响应后,会把镜像持久化到应用所在的数据源的 Undo Log 表中,最后提交本地事务。


以上所有操作都会保证在同一个本地事务中,保证业务操作和 Undo Log 操作的原子性。

一阶段


image

理解了单个应用的处理流程,再从一个完全的调用链路,去看 Seata 的处理过程,相信理解起来会简单很多。


1、首先一个使用了 @GlobalTransactional 的接口被调用,Seata 会对其进行拦截,拦截的角色我们称之为 TM,这个时候会访问 TC 开启一个新的全局事务,TC 收到请求后会生成 XID 和全局事务信息并持久化,然后返回 XID。

2、在每一层的调用链路中,XID 都必须往下传递,然后每一层都经过之前说过的处理逻辑,直到执行完成/异常抛出。


直到目前,一阶段已经执行完成。


另外一个需要注意的问题是,如果发现事务不生效,需要检查 XID 是否成功往下传递。

二阶段提交

image

如果在整个调用链路的过程,没有发生任何异常,那么二阶段提交的过程是非常简单而且非常的高效,只有两步:


  • TC 清理全局事务对应的信息

  • RM 清理对应 Undo Log 信息

二阶段回滚


image

若调用过程中出现异常,会自动触发反向回滚:


反向回滚表示,如果调用链路顺序为 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@Transactionalpublic PayMoneyDto detail(ProcessOnEventRequestDto processOnEventRequestDto) { return baseMapper.detail(processOnEventRequestDto.getProcessInfoDto().getBusinessKey())}@Mapperpublic 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 的查询语句进行代理:


image

如果一个全局事务 1 正在操作,并且未进行二阶段提交/回滚的时候,全局锁是被全局事务 1 锁持有的,同时另外一个全局事务 2 尝试去查询相同的数据,由于查询语句被代理,Seata 会尝试去获取这条数据的全局锁,直到获取成功/失败(重试次数达到配置值)为止。

问题

在生产上运行接近 1 年时间,总体来说遇到的问题不算多,解决起来也比较容易,比如以下这个问题:



经过排查发现,由于 Seata 会使用 jdbc 标准接口尝试获取业务操作所对应的表结构,由于表结构改动频率较少,并且考虑到表结构变更后应用会进行重启,所以会对表结构进行缓存,如果表结构改动后不对应用进行重启,有可能引发构建镜像时出现 NullPointerException。下面贴出关键代码:



@Overridepublic 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 查询分支事务时,数据未同步到从库,导致遗漏了一部分分支事务数据。


源码部分


@Overridepublic 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(); }}
复制代码



@Overridepublic 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 一下代码。

部署-高可用


image

Seata 和其他中间件的高可用部署方式差别不大,如图片所示,确保应用服务和 TC 访问相同的注册中心和配置中心,同时只需要启动多台 TC,并将 store.mode 改为 db 模式即可完成高可用部署,并选择合适的注册中心和配置中心即可,目前支持的配置中心有:

  • nacos

  • consul

  • etcd3

  • eureka

  • redis

  • sofa

  • zookeeper


可选的配置中心有:

  • nacos

  • etcd3

  • consul

  • apollo

  • zk

部署-单节点多应用

image

当然也有更加灵活的部署方式,通过 vgoup-mapping(事务集群),可以做到单节点多应用的隔离,比如 A 应用和 B 应用访问 A-Group 的两个 TC,C 应用和 D 应用访问 B-Group 的两个 TC,E 应用和 F 应用访问 C-Group 的两个 TC。

部署-异地容灾


image


image

通过 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 = "" }}
复制代码

Demo

最后通过访问阿里云知行动手首页,即可在线快速体验各种各样的中间件:


https://start.aliyun.com


Seata 直达传送门,无需下载代码,在线编译和部署:


https://start.aliyun.com/handson/isnEO76f/distributedtransaction


作者介绍:

作者 | 张嘉伟(GitHub ID:l81893521),就职于逸仙电商交易中心;Seata Committer,加入 Seata 社区已有一年半,见证了从 Fescar 到 Seata 的变更,GA 等。


本文转自:阿里巴巴中间件(ID:Aliware_2018)

原文链接:逸仙电商Seata企业级落地实践

2021-04-25 13:001734

评论

发布
暂无评论
发现更多内容

Native开发工具之CPU 和架构(三),学习Android开发的步骤,

android 程序员 移动开发

OOM问题原理解析(二),移动端开发技术

android 程序员 移动开发

MVVM系列之二:LiveData,android程序开发教程

android 程序员 移动开发

springmvc的定时任务

小鲍侃java

11月日更

MVVM系列之一:Lifecycle,面试竟然被这31道Android基础题难倒了

android 程序员 移动开发

OkHttp 3,安卓移动开发大作业

android 程序员 移动开发

OkHttp流程分析,音视频编解码技术

android 程序员 移动开发

OkHttp 断点上传的“基操”(1),完美讲解内存缓存LruCache实现原理

android 程序员 移动开发

OKio源码分析(1)six sy007 情感导师,android面试题2019

android 程序员 移动开发

毕业总结和毕业设计

cherrycheek

【等保小知识】等保与关保两者之间有啥区别?

行云管家

网络安全 等级保护 分保 关保

Okhttp的缓存机制,原理讲解

android 程序员 移动开发

一站式智能化是采购数字化的大趋势

WorkPlus Lite

网易云信亮相 LiveVideoStackCon 2021,解构自研大规模传输网 WE-CAN

网易云信

通信云 传输协议

MotionLayout_ 打开动画新世界大门 (part II),android插件化原理

android 程序员 移动开发

Java线程安全ReentrantLock

FunTester

Java 性能测试 线程安全 测试开发 FunTester

「元宇宙」赛道,除了脸书,微软也来了

WorkPlus Lite

OkHttp 断点上传的“基操”,算法题+JVM

android 程序员 移动开发

分享 | 一文了解 PG PITR 即时恢复

RadonDB

数据库 postgresql RadonDB

React Native Android 源码框架浅析(主流程及 Java 与 JS 双边通信)

android 程序员 移动开发

React Native Android混合开发实战教程(1),flutter瀑布流

android 程序员 移动开发

React Native Android混合开发实战教程,Android入门你值得拥有

android 程序员 移动开发

实验室信息系统的主要功能及作用

低代码小观

企业管理 管理系统 LIMS实验室信息管理系统 信息管理系统 实验室

QQ音乐Android编译提速之路,腾讯T2大牛亲自讲解

android 程序员 移动开发

具有中国特色的堡垒机到底有用吗?有什么用?

行云管家

网络安全 信息安全 数据安全 堡垒机

《黑客之道》干了一夜的kali Linux之Metasploit渗透测试框架的基本使用

学神来啦

Linux 运维 黑客 渗透 Metasploit

MotionLayout_ 打开动画新世界大门 (part II)(1),kotlin框架

android 程序员 移动开发

OkHttp3源码详解之拦截器(四),计算机应届毕业生面试题

android 程序员 移动开发

直呼内行!阿里大佬离职带出内网专属“高并发系统设计”学习笔记

编程 程序员 消息队列 高并发系统

React Native 与 嵌入Android原生与Activity页面互相跳转(1)

android 程序员 移动开发

python3如何安装MySQLdb库

YUKI0506

Python3 mysqldb

逸仙电商Seata企业级落地实践_架构_阿里巴巴中间件_InfoQ精选文章