138-150-springcloud-seata:
分布式事务问题由来:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
Seata
1、seata是什么
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
官网地址:https://seata.io/zh-cn/
能干嘛,一个典型的分布式事务过程,分布式事务处理过程的一ID+三组件模型:
1、Transaction ID XID 全局唯一的事务ID
2、三组件概念
2.1、TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
2.2、TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
2.3、RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
处理过程:
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
2、Seata-server安装
https://github.com/seata/seata/releases
我们只需要使用一个 @GlobalTransactional 注解在业务方法上:
Seata-Server安装,官网地址 - http://seata.io/zh-cn/
下载版本 - 0.9.0,seata-server-0.9.0.zip解压到指定目录并修改conf目录下的file.conf配置文件
先备份原始file.conf文件
主要修改:自定义事务组名称+事务日志存储模式为db +数据库连接信息
file.conf
service {
##fsp_tx_group是自定义的
vgroup_mapping.my.test.tx_group="fsp_tx_group"
}
## transaction log store
store {
## store mode: file, db
## 改成db
mode = "db"
# database store
db {
.....
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "密码"
}
}
mysql数据库新建库seata,在seata库里建表
建表db_store.sql在\seata-server-0.9.0\seata\conf目录里面
修改seata-server-0.9.0\seata\conf目录下的registry.conf配置文件
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 改用为nacos
type = "nacos"
nacos {
## 加端口号
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
...
}
目的是:指明注册中心为nacos,及修改nacos连接信息
先启动Nacos端口号8848 nacos\bin\startup.cmd
再启动seata-server - seata-server-0.9.0\seata\bin\seata-server.bat
3、Seata尚硅谷案例练习
1、业务数据库准备
CREATE DATABASE seata_order;存储订单的数据库;
CREATE DATABASE seata_storage;存储库存的数据库;
CREATE DATABASE seata_account;存储账户信息的数据库。
####seata_order库下建t_order表
CREATE TABLE t_order (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
##seata_storage库下建t_storage表
CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');
SELECT * FROM t_storage;
##seata_account库下建t_account表
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
#订单-库存-账户3个库下都需要建各自的回滚日志表\seata-server-0.9.0\seata\conf目录下的db_ undo_ log.sql建表SQL
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) 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) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
seata-order-service2001 seata-storage-service2002 seata-account-service2003
测试下单 - http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
超时异常,没加@GlobalTransactional和加了做比较
4、Seata原理
4.1、分布式事务的执行流程:
- TM开启分布式事务(TM向TC注册全局事务记录) ;
- 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态) ;
- TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务) ;
- TC汇总事务信息,决定分布式事务是提交还是回滚;
- TC通知所有RM提交/回滚资源,事务二阶段结束。
4.2、AT模式如何做到对业务的无侵入
什么是seata:https://seata.io/zh-cn/docs/overview/what-is-seata.html
前提
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
整体机制
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
一阶段加载,在一阶段,Seata会拦截“业务SQL”
- 解析SQL语义,找到“业务SQL" 要更新的业务数据,在业务数据被更新前,将其保存成"before image”
- 执行“业务SQL" 更新业务数据,在业务数据更新之后,
- 其保存成"after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。
- 二阶段提交
二阶段如果顺利提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
-
二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务SQL",还原业务数据。回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。
补充:
雪花算法
借鉴摘抄自:https://www.bilibili.com/video/BV18E411x7eT?p=148
为什么需要分布式全局唯一ID以及分布式ID的业务需求?集群高并发情况下如何保证分布式唯一全局Id生成?
在复杂分布式系统中,往往需婴对大量的数据和消息进行唯一标识,如在美团点评的金融、支付、餐饮、酒店,猫眼电影等产品的系统中数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息。特别一点的如订单、骑手、优惠券也都雷要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。
1、ID生成规则部分硬性要求
全局唯一:不能出现重复的ID号,既然是唯一-标识,这是最基本的要求
趋势递增:在MySQL的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求
信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可。如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,需要ID无规则不规则,让竞争对手否好猜。
含时间戳:这样就能够在开发中快速了解这个分布式id的生成时间。
2、ID号生成系统的可用性要求
高可用:发一个获取分布式ID的请求,服务器就要保证99.999%的情况下给我创建一个唯一分布式ID。
低延迟:发一个获取分布式ID的请求,服务器就要快,极速。
高QPS:假如并发一口气10万个创建分布式ID请求同时杀过来,服务器要顶的住且一下子成功创建10万个分布式ID。
一般通用方案
UUID
UUID(Universally Unique ldentifer)的标准型式包含32个16进制数字,以连了号分为五段,形式为8-4-4-4-12的36个字符, 示例:550e8400-e29b-41d4-a716-446655440000
性能非常高:本地生成,没有网络消耗,如果只考虑唯一性,ok。但是,入数据库性能差
为什么无序的UUID会导致入库性能变差呢?(数据库的B+树排序修改等操作,数据库和数据结构这里不做讲解。)
mysql: https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html
无序,无法预测他的生成顺序,不能生成递增有序的数字。首先分布式ID一般都会作为主键, 但是安装MySQL官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐。
主键,ID作为主键时在特定的环境会存在一些问题。比如做DB主键的场景下,UUID就非常不适用MySQL官方有明确的建议主键要尽量越短越好36个字符长度的UUID不符合要求。
索引,既然分布式ID是主键,然后主键是包含索引的,然后MySQL的索引是通过B+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的B+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键地械的B+树进行很大的修改,这一点很不好。 插入完全无序,不但会导致一-些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。
数据库自增主键
单机
在单机里面,数据库的自增ID机制的主要原理是:数据库自增ID和MySQL数据库的replace into实现的。
REPLACE INTO的含义是插入一条记录,如果表中唯一索引的值遇到冲突,则替换老数据。
这里的replace into跟inset功能类似,不同点在于:replace into首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入。否则直接插入新数据。
Twitter的分布式自增ID算法snowflake
概述
Twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra(由Facebook开发一套开源分布式NoSQL数据库系统)。因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一生成服务。
Twitter的分布式雪花算法SnowFlake ,经测试snowflake 每秒能够产生26万个自增可排序的ID
- Twitter的SnowFlake生成ID能够按照时间有序生成。
- SnowFlake算法生成ID的结果是一个64bit大小的整数, 为一个Long型(转换成字符串后长度最多19)。
- 分布式系统内不会产生ID碰撞(由datacenter和workerld作区分)并且效率较高。
分布式系统中,有一些需要使用全局唯一ID的场景, 生成ID的基本要求:
- 在分布式的环境下必须全局且唯一 。
- 一般都需要单调递增,因为一般唯一ID都会存到数据库,而Innodb的特性就是将内容存储在主键索引树上的叶子节点而且是从左往右,递增的,所以考虑到数据库性能,一般生成的ID也最好是单调递增。 为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点, 首先他相对比较长, 另外UUID一般是无序的。
- 可能还会需要无规则,因为如果使用唯一ID作为订单号这种,为了不然别人知道一天的订单量是多少,就需要这个规则。
结构
雪花算法的几个核心组成部分:
号段解析:
1bit:
不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
41bit - 时间戳,用来记录时间戳,毫秒级:
- 41位可以表示2^41 − 1个数字。
- 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0至2^41 − 1减1是因为可表示的数值范围是从0开始算的,而不是1。
- 也就是说41位可以表示2^41−1个毫秒的值,转化成单位年则是( 2 41 − 1 ) / ( 1000 ∗ 60 ∗ 60 ∗ 24 ∗ 365 ) = 6969年。
10bit - 工作机器ID,用来记录工作机器ID:
- 可以部署在2 10 = 1024 2^{10}=1024个节点,包括5位DataCenterId和5位Workerld。
- 5位(bit) 可以表示的最大正整数是2^5 − 1 = 31,即可以用0、1、2、3、…31这32个数字,来表示不同的DataCenterld或Workerld。
12bit - 序列号,用来记录同毫秒内产生的不同id。
12位(bit) 可以表示的最大正整数是2^12 − 1 = 40954095, 即可以用0、1、2、 3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
SnowFlake可以保证:
所有生成的ID按时间趋势递增。
整个分布式系统内不会产生重复id(因为有DataCenterId和Workerld来做区分)
源码
以下代码仅供学习:
git:
Hutool的Snowflake文档
添加依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>4.6.8</version>
</dependency>
示例程序:
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Slf4j
@Component
public class IdGeneratorSnowflake{
private long workerId = 0;
private long datacenterId = 1;
private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
public synchronized long snowflakeId(){
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long datacenterId){
Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
public static void main(String[] args){
IdGeneratorSnowflake idGenerator = new IdGeneratorSnowflake();
System.out.println(idGenerator.snowflakeId());
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 20; i++){
threadPool.submit(() -> {
System.out.print1n(idGenerator.snowflakeId());
});
}
threadPool.shutdown();
}
优缺点
优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位,非常灵活。
缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成。
在单机上是递增的,但是由于设计到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况。
(此缺点可以认为无所谓,一般分布式ID只要求趋势递增,并不会严格要求递增,90%的需求都只要求趋势递增)
其他补充
百度开源的分布式唯一ID生成器UidGenerator
美团点评分布式ID生成系统Leaf
学习路径:https://space.bilibili.com/302417610/,如有侵权,请联系q进行删除:3623472230