分布式事物处理_认识本地事物
什么是事物
事务就是针对数据库的一组操作,它可以由一条或多条SQL语句组 成,同一个事务的操作具备同步的特点,事务中的语句要么都执 行,要么都不执行。
举个栗子:
你去小卖铺买东西,一手交钱,一手交货就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。
什么是本地事物
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利 用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用 主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
数据库事务的四大特性ACID
总结
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个 不可分割的执行单元,该执行单元中的所有操作要么都成功,要么 都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。
关系型数据库事务基础_并发事务带来的问题
并发事务带来的问题
数据库一般会并发执行多个事务,而多个事务可能会并发地对相同 的数据进行增加、删除、修改和查询操作,进而导致并发事务问 题。
脏写
当两个或两个以上的事务选择数据库中的同一行数据,并基于最初 选定的值更新该行数据时,因为每个事务之间都无法感知彼此的存 在,所以会出现最后的更新操作覆盖之前由其他事务完成的更新操 作的情况。也就是说,对于同一行数据,一个事务对该行数据的更新操作覆盖了其他事务对该行数据的更新操作。
解决方案: 让每个事物按照顺序串行的方式执行,按照一定的顺序一次进行写操作。
脏读
一个事务正在对数据库中的一条记录进行修改操作,在这个事务完 成并提交之前,当有另一个事务来读取正在修改的这条数据记录 时,如果没有对这两个事务进行控制,则第二个事务就会读取到没 有被提交的脏数据,并根据这些脏数据做进一步的处理,此时就会 产生未提交的数据依赖关系。我们通常把这种现象称为脏读,也就是一个事务读取了另一个事务未提交的数据。
解决方案: 先写后读,也就是写完之后再读。
不可重复读
一个事务读取了某些数据,在一段时间后,这个事务再次读取之前 读过的数据,此时发现读取的数据发生了变化,或者其中的某些记录已经被删除,这种现象就叫作不可重复读。
解决方案: 先读后写,也就是读完之后再写。
幻读
一个事务按照相同的查询条件重新读取之前读过的数据,此时发现 其他事务插入了满足当前事务查询条件的新数据,这种现象叫作幻读。
解决方案: 先读后写,也就是读完之后再写。
关系型数据库事务基础_MySQL事务隔离级别
MySQL中的InnoDB储存引擎提供SQL标准所描述的4种事务隔离级 别,分别为
读未提交 (Read Uncommitted)
读已提交 (ReadCommitted)
可重复读(Repeatable Read)
串行化 (Serializable)。
1、读未提交(Read Uncommitted):事务可以读取未提交的数据,也称作脏读(Dirty Read)。一 般很少使用。
2、读已提交(Read Committed):是大都是 DBMS (如:Oracle, SQLServer)默认事务隔离。执行两次同意的查询却有不同的结果,也叫不可重复读。
3、可重复读(Repeable Read):是 MySQL 默认事务隔离级别。能确保同一事务多次读取同一数据 的结果是一致的。可以解决脏读的问题,但理论上无法解决幻读(Phantom Read)的问题。
4、可串行化(Serializable):是最高的隔离级别。强制事务串行执行,会在读取的每一行数据上加锁,这样虽然能避免幻读的问题,但也可能导致大量的超时和锁争用的问题。很少会应用到这种级别,只有在非常需要确保数据的一致性且可以接受没有并发的应用场景下才会考虑。
MySQL事务隔离级别_模拟异常发生之脏读
前置知识
# 查看 MySQL 版本
select version();
# 开启事务
start transaction;
# 提交事务
commit;
# 回滚事务
rollback;
查看连接的客户端详情
每个 MySQL 命令行窗口就是一个 MySQL 客户端,每个客户端都可 以单独设置(不同的)事务隔离级别,这也是演示 MySQL 并发事务的基础。
show processlist;
新建数据库和测试数据
-- 创建数据库
drop database if exists testdb;
create database testdb;
use testdb;
-- 创建表
create table userinfo(
id int primary key auto_increment,
name varchar(250) not null,
balance decimal(10,2) not null default 0
);
-- 插入测试数据
insert into userinfo(id,name,balance)
values(1,'Java',100),(2,'MySQL',200);
查询事务的隔离级别
select
@@global.transaction_isolation,@@transaction_is
olation;
设置客户端的事务隔离级别
通过以下 SQL 可以设置当前客户端的事务隔离级别:
set session transaction isolation level 事务隔离 级别;
脏读
一个事务读到另外一个事务还没有提交的数据,称之为脏读。脏读 演示的执行流程如下:
脏读演示步骤1
设置窗口 2 的事务隔离级别为读未提交,设置命令如下:
set session transaction isolation level read
uncommitted;
注意: 事务隔离级别读未提交存在脏读的问题。
脏读演示步骤2
窗口2开启事务,查询用户表如下图所示:
start transaction;
select * from userinfo;
注意: 从上述结果可以看出,在窗口 2 中读取到了窗口 1 中事务未提 交的数据,这就是脏读。
脏读演示步骤3
在窗口 1 中开启一个事务,并给 Java 账户加 50 元,但不提交事 务,执行的 SQL 如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update userinfo set balance=balance+50
where name="java";
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
脏读演示步骤4
在窗口 2 中再次查询用户列表,执行结果如下:
注意: 从上述结果可以看出,在窗口 2 中读取到了窗口 1 中事务未提交的数据,这就是脏读。
不可重复读
不可重复读是指一个事务先后执行同一条 SQL,但两次读取到的数据不同,就是不可重复读。不可重复读演示的执行流程如下:
不可重复读演示步骤1
设置窗口 2 的事务隔离级别为读已提交
set session transaction isolation level read
committed;
注意: 读已提交可以解决脏读的问题,但存在不可重复读的问题。
不可重复读演示步骤2
在窗口 2 中开启事务,并查询用户表,执行结果如下
不可重复读演示步骤3
在窗口 1 中开启事务,并给 Java 用户添加 20 元,但不提交事务, 再观察窗口 2 中有没有脏读的问题,具体执行结果如下图所示:
从上述结果可以看出,当把窗口的事务隔离级别设置为读已提交, 已经不存在脏读问题了。接下来在窗口 1 中提交事务,执行结果如下图所示:
不可重复读演示步骤4
切换到窗口 2 中再次查询用户列表,执行结果如下:
不可重复读和脏读的区别:
脏读可以读到其他事务中未提交的数据,而不可重复读是读取到了其他事务已经提交的数据,但前后两次读取的结果不同。
幻读
幻读名如其文,它就像发生了某种幻觉一样,在一个事务中明明没 有查到主键为 X 的数据,但主键为 X 的数据就是插入不进去,就像某种幻觉一样。幻读演示的执行流程如下:
幻读演示步骤1
在窗口1和窗口2修改事务隔离级别为可重复读。
set session transaction isolation level
repeatable read;
幻读演示步骤2
设置窗口 2 为可重复读,可重复有幻读的问题,查询编号为 3 的用户,具体执行 SQL 如下:
start transaction;
select * from userinfo where id=3;
注意:从上述结果可以看出,查询的结果中 id=3 的数据为空。
幻读演示步骤3
开启窗口 1 的事务,插入用户编号为 3 的数据,然后成功提交事务,执行 SQL 如下:
start transaction;
insert into userinfo(id,name,balance)
values(3,'Spring',100);
commit;
幻读演示步骤4
在窗口 2 中插入用户编号为 3 的数据,执行 SQL 如下:
insert into userinfo(id,name,balance)
values(3,'Spring',100);
注意: 添加用户数据失败,提示表中已经存在了编号为 3 的数据,且 此字段为主键,不能添加多个。
幻读演示步骤5
在窗口 2 中,重新执行查询:
select * from userinfo where id=3;
注意: 在此事务中查询明明没有编号为 3 的用户,但插入的时候却却 提示已经存在了,这就是幻读。
不可重复读和幻读的区别
二者描述的则重点不同,不可重复读描述的侧重点是修改操作,而 幻读描述的侧重点是添加和删除操作。
MySQL中锁的分类
从本质上讲,锁是一种协调多个进程或多个线程对某一资源的访问 的机制,MySQL使用锁和MVCC机制实现了事务隔离级别。
锁的分类
悲观锁和乐观锁
悲观锁
顾名思义,悲观锁对于数据库中数据的读写持悲观态度,即在整个数据处理的过程中,它会将相应的数据锁定。在数据库中,悲观锁的实现需要依赖数据库提供的锁机制,以保证对数据库加锁后,其他应用系统无法修改数据库中的数据。
注意:
在悲观锁机制下,读取数据库中的数据时需要加锁,此时不能对这些数据进行修改操作。修改数据库中的数据时也需要加锁,此时不能对这些数据进行读取操作。
乐观锁
悲观锁会极大地降低数据库的性能,特别是对长事务而言,性能的损耗往往是无法承受的。乐观锁则在一定程度上解决了这个问题。
注意:
实现乐观锁的一种常用做法是为数据增加一个版本标识,如果是通过数据库实现,往往会在数据表中增加一个类似version的版本号字段。
读锁和写锁
读锁
读锁又称为共享锁,共享锁就是多个事务对于同一数据可以共享一 把锁,都能访问到数据,但是只能读不能修改。
写锁
写锁又称为排他锁,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁, 包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
注意:
需要注意的是,对同一份数据,如果加了读锁,则可以继续为 其加读锁,且多个读锁之间互不影响,但此时不能为数据增加 写锁。一旦加了写锁,则不能再增加写锁和读锁。因为读锁具有共享性,而写锁具有排他性。
表锁、行锁和页面锁
表锁
表锁也称为表级锁,就是在整个数据表上对数据进行加锁和释放锁。典型特点是开销比较小,加锁速度快,一般不会出现死锁,锁定的粒度比较大,发生锁冲突的概率最高,并发度最低。
手动增加表锁
mysql> lock table userinfo read;
Query OK, 0 rows affected (0.00 sec)
mysql> lock table userinfo write;
Query OK, 0 rows affected (0.00 sec)
查看数据表上增加的锁
show open tables;
删除表锁
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
行锁
行锁也称为行级锁,就是在数据行上对数据进行加锁和释放锁。典 型特点是开销比较大,加锁速度慢,可能会出现死锁,锁定的粒度最小,发生锁冲突的概率最小,并发度最高。
页面锁
页面锁也称为页级锁,就是在页面级别对数据进行加锁和释放锁。 对数据的加锁开销介于表锁和行锁之间,可能会出现死锁,锁定的粒度大小介于表锁和行锁之间,并发度一般。
间隙锁和临键锁
间隙锁
在MySQL中使用范围查询时,如果请求共享锁或排他锁,InnoDB 会给符合条件的已有数据的索引项加锁。如果键值在条件范围内, 而这个范围内并不存在记录,则认为此时出现了“间隙(也就是 GAP)”。InnoDB存储引擎会对这个“间隙”加锁,而这种加锁机制就是间隙锁(GAP Lock)。
例如,userinfo数据表中存在如下数据。
解释:
此时,userinfo数据表中的间隙包括id为(3,15]、(15,20]、 (20,正无穷]的三个区间。如果执行如下命令,将符合条件的用 户的账户余额增加100元。 update userinfo set balance = balance + 100 where id > 5 and id <16; 则其他事务无法在(3,20]这个区间内插入或者修改任何数据。 这里需要注意的是,间隙锁只有在可重复读事务隔离级别下才 会生效。
临键锁
临键锁(Next-Key Lock)是行锁和间隙锁的组合,例如上面例子中 的区间(3,20]就可以称为临键锁。
MySQL中的死锁问题
什么是死锁
死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并 发读写请求场景中。当两个及以上的事务,双方都在等待对方释放 已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出 现“死锁”。
Deadlock found when trying to get lock...
举例来说 A 事务持有 X1 锁 ,申请 X2 锁,B事务持有 X2 锁,申请 X1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。
第一步
打开终端A,登录MySQL,将事务隔离级别设置为可重复读,开启 事务后为userinfo数据表中id为1的数据添加排他锁,如下所示。
mysql> set session transaction isolation level
repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | Java | 100.00 |
| 2 | MySQL | 200.00 |
+----+-------+---------+
2 rows in set (0.00 sec)
第二步
打开终端B,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为userfinfo数据表中id为2的数据添加排他锁,如下所示。
mysql> set session transaction isolation level
repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo where id = 2;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 2 | MySQL | 200.00 |
+----+-------+---------+
1 row in set (0.00 sec)
第三步
在终端A为userinfo数据表中id为2的数据添加排他锁,如下所示。
mysql> select * from userinfo where id =2 for
update;
注意: 此时,线程会一直卡住,因为在等待终端B中id为2的数据释放排他锁。
第四步
在终端B中为userinfo数据表中id为1的数据添加排他锁,如下所示。
mysql> select * from userinfo where id =1 for
update;
ERROR 1213 (40001): Deadlock found when trying
to get lock; try restarting transaction
通过如下命令可以查看死锁的日志信息。
show engine innodb status\G
注意:
通过命令行查看LATEST DETECTED DEADLOCK选项相关的信 息,可以发现死锁的相关信息,或者通过配置 innodb_print_all_deadlocks(MySQL 5.6.2版本开始提供)参数为ON,将死锁相关信息打印到MySQL错误日志中。
如何避免死锁
MySQL事务的实现原理_什么是redo log
MySQL的事务实现离不开Redo Log和Undo Log。从某种程度上 说,事务的隔离性是由锁和MVCC机制实现的,原子性和持久性是 由Redo Log实现的,一致性是由Undo Log实现的。
什么是redo log
redo log叫做重做日志,是用来实现事务的持久性。该日志文件由 两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件 (redo log),前者是在内存中,后者在磁盘中。当事务提交之后会 把所有修改信息都会存到该日志中。
注意:
先写日志,再写磁盘的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术。
Redo Log刷盘规则
在计算机操作系统中,用户空间( user space )下的缓冲区数据一般情况 下是无法直接写入磁盘的,中间必须经过操作系统内核空间( kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo log file 实际上是先 写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file 中。
mysql 支持三种将 redo log buffer 写入 redo log file 的时机。 innodb_flush_log_at_trx_commit。
Redo Log刷盘最佳实践
不同的Redo Log刷盘规则,对MySQL数据库性能的影响也不同。
创建测试数据库
create database if not exists test;
create table flush_disk_test(
id int not null auto_increment,
name varchar(20),
primary key(id)
)engine=InnoDB;
编写存储过程
为了测试方便,这里创建一个名为insert_data的存储过程,接收一 个int类型的参数。这个参数表示向flush_disk_test数据表中插入的记录行数。
drop procedure if exists insert_data;
-- 该段命令是否已经结束了,mysql是否可以执行了。
delimiter $$
create procedure insert_data(i int)
begin
-- 声明变量 s
declare s int default 1;
-- 声明变量 c
declare c varchar(50) default 'binghe';
-- while循环
while s<=i do
-- 开启事务
start transaction;
-- 添加数据
insert into flush_disk_test (name) values(c);
-- 提交事务
commit;
-- s变量累加
set s=s+1;
-- 循环结束
end while;
end$$
-- 该段命令是否已经结束了,mysql是否可以执行了。
delimiter ;
查看刷盘规则
show variables like
'innodb_flush_log_at_trx_commit';
第一步
将innodb_flush_log_at_trx_commit变量的值设置为0。
set global innodb_flush_log_at_trx_commit=0;
调用insert_data向flush_disk_test数据表中插入10万条数据,如下 所示。
mysql> call insert_data (100000);
Query OK, 0 rows affected (2.18 sec)
注意: 可以看到,当innodb_flush_log_at_trx_commit变量的值设置为0时,向表中插入10万条数据耗时2.18s。
第二步
将innodb_flush_log_at_trx_commit变量的值设置为1。
set global innodb_flush_log_at_trx_commit=1;
调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。
mysql> call insert_data (100000);
Query OK, 0 rows affected (16.18 sec)
注意:
可以看到,当innodb_flush_log_at_trx_commit变量的值设置为1时,向表中插入10万条数据耗时16.18s。
第三步
将innodb_flush_log_at_trx_commit变量的值设置为2。
set global innodb_flush_log_at_trx_commit=2;
调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。
mysql> call insert_data (100000);
Query OK, 0 rows affected (3.05 sec)
注意:
可以看到,当innodb_flush_log_at_trx_commit变量的值设置为2时,向表中插入10万条数据耗时3.05s。
结论
MySQL事务的实现原理_什么是undo log
undo log的概念
undo log是mysql中比较重要的事务日志之一,顾名思义,undo log是一种用于撤销回退的日志,在事务没提交之前,MySQL会先 记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
undo log的作用
在MySQL中,undo log日志的作用主要有两个:
1、提供回滚操作
---修改之前name = 张三 update user set name = "李四" where id = 1; ----此时undo log会记录一条相反的update语句,如下: update user set name = "张三" where id = 1;
注意: 如果这个修改出现异常,可以使用undo log日志来实现回滚操作,以保证事务的一致性。
2、提供多版本控制(MVCC)MVCC,即多版本控制。在MySQL数据库InnoDB存储引擎中,用 undo Log来实现多版本并发控制(MVCC)。当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据版本是怎样的,从而让用户能够读取到当前事务操作之前的数据【快照读】。
快照读:
SQL读取的数据是快照版本【可见版本】,也就是历史版本,不用加锁,普通的SELECT就是快照读。
当前读:
SQL读取的数据是最新版本。通过锁机制来保证读取的数据无法 通过其他事务进行修改UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE都是当前 读。
undo log的存储机制
undo log的存储由InnoDB存储引擎实现,数据保存在InnoDB的数据文件中。在InnoDB存储引擎中,undo log是采用分段(segment) 的方式进行存储的。
undo log的工作原理
在更新数据之前,MySQL会提前生成undo log日志,当事务提交的时候,并不会立即删除undo log,因为后面可能需要进行回滚操 作,要执行回滚(rollback)操作时,从缓存中读取数据。undo log日志的删除是通过通过后台purge线程进行回收处理的。
总结
undo log是用来回滚数据的用于保障未提交事务的原子性。
分布式事物处理_认识分布式事物
前言
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式 应用,下图描述了单体应用向微服务的演变。
注意:
分布式系统会把一个应用系统拆分为可独立部署的多个服务, 因此需要服务与服务之间远程协作才能完成事务操作,这种分 布式系统环境下由不同的服务之间通过网络远程协作完成事务 称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
假如没有分布式事务
解释:
上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单 之前需要先调用库存服务,进行扣除库存,再调用订单服务, 创建订单记录。
正常情况下,两个数据库各自更新成功,两边数据维持着一致性。
但是,在非正常情况下,有可能库存的扣减完成了,随后的订单记 录却因为某些原因插入失败。这个时候,两边数据就失去了应有的一致性。
问题: 这种时候需要要保证数据的一致性,单数据源的一致性靠单机 事物来保证,多数据源的一致性就要靠分布式事物保证。
什么是分布式事务
指一次大的操作由不同的小操作组成的,这些小的操作分布在不同 的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
分布式架构的理论知识_CAP理论
前言
分布式系统正变得越来越重要,大型网站几乎都是分布式的。分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。
1998年,加州大学的计算机科学家Eric Brewer 提出,分布式系统有三个指标。
它们的第一个字母分别是 C、A、P。这三个指标不可能同时做到。 这个结论就叫做CAP 定理。
分区容错性
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个 区。分区容错的意思是,区间通信可能失败。比如,一台服务器放 在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
结论:
分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
一致性
Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操 作,将其改为 v1。
接下来,用户的读操作就会得到 v1。这就叫一致性。
问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。
为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发 送一条消息,要求 G2 也改成 v1。
这样的话,用户向 G2 发起读操作,也能得到 v1。
可用性
只要收到用户的请求,服务器就必须给出回应。
一致性和可用性的矛盾
解释:
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的 读操作和写操作。只有数据同步后,才能重新开放读写。锁定 期间,G2 不能读写,没有可用性。如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。
一致性和可用性如何选择
分布式事物处理_分布式事务产生的场景
跨JVM进程
当我们将单体项目拆分为分布式、微服务项目之后,各个服务之间通过远程REST或者RPC调用来协同完成业务操作。
典型的场景:
商城系统中的订单微服务和库存微服务,用户在下单时会访问 订单微服务,订单微服务在生成订单记录时,会调用库存微服务来扣减库存。各个微服务是部署在不同的JVM进程中的,此时,就会产生因跨JVM进程而导致的分布式事务问题。
跨数据库实例
单体系统访问多个数据库实例,也就是跨数据源访问时会产生分布式事务。
典型的场景:
例如,我们的系统中的订单数据库和交易数据库是放在不同的 数据库实例中,当用户发起退款时,会同时操作用户的订单数 据库和交易数据库,在交易数据库中执行退款操作,在订单数 据库中将订单的状态变更为已退款。由于数据分布在不同的数 据库实例,需要通过不同的数据库连接会话来操作数据库中的 数据,此时,就产生了分布式事务。
多个服务数据库
多个微服务访问同一个数据库。
分布式事物解决方案_强一致性分布式事务之2PC模型
两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两类节点:一个是中心化 协调者节点 和 N个参与者节点 。
生活中的2PC
A组织B、C和D三个人去爬山:如果所有人都同意去爬山,那么活动将举行;如果有一人不同意去爬山,那么活动将取消。
首先A将成为该活动的协调者,B、C和D将成为该活动的参与者。
具体流程:
阶段1:
①A发邮件给B、C和D,提出下周三去爬山,问是否同意。 那么此时A需要等待B、C和D的邮件。
②B、C和D分别查看自己的日程安排表。B、C发现自己在 当日没有活动安排,则发邮件告诉A它们同意下周三去爬山。由 于某种原因, D白天没有查看邮 件。那么此时A、B和C均需要 等待。到晚上的时候,D发现了A的邮件,然后查看日程安排, 发现周三当天已经有别的安排,那么D回复A说活动取消吧。
阶段2:
①此时A收到了所有活动参与者的邮件,并且A发现D下周 三不能去爬山。那么A将发邮件通知B、C和D,下周三爬山活动 取消。
②此时B、C回复A“太可惜了”,D回复A“不好意思”。至此该 事务终止。
2PC阶段处理流程
举例订单服务A,需要调用支付服务B去支付,支付成功则处理购物 订单为待发货状态,否则就需要将购物订单处理为失败状态。
第一阶段:投票阶段
分布式事物解决方案_XA方案
什么是DTP
2PC的传统方案是在数据库层面实现的,如Oracle、MySQL都支持 2PC协议,为了统一标准减少行业内不必要的对接成本,需要制定 标准化的处理模型及接口标准,国际开放标准组织Open Group定 义分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
分布式事物解决方案_Seata实现
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简 单易用的分布式事务服务。Seata 为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata整体框架
全局事务与分支事务的关系图
与传统2PC的模型类似,Seata定义了三个组件来协议分布式事务的处理过程
还拿新用户注册送积分举例Seata的分布式事务过程
执行流程 :
Seata实现2PC与传统2PC的差别
Seata提供XA模式实现分布式事务_业务说明
业务说明
本实例通过Seata中间件实现分布式事务,模拟两个账户的转账交易 过程。两个账户在两个不同的银行(张三在bank1、李四在 bank2),bank1和bank2是两个微服务。交易过程中,张三给李四 转账制定金额。上述交易步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。
工程环境
创建数据库
bank1库,包含张三账户
CREATE DATABASE /*!32312 IF NOT EXISTS*/`bank1`
/*!40100 DEFAULT CHARACTER SET utf8 */;
USE `bank1`;
/*Table structure for table `account_info` */
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) COLLATE utf8_bin
DEFAULT NULL COMMENT '户主姓名'
,
`account_no` varchar(100) COLLATE utf8_bin
DEFAULT NULL COMMENT '银行卡号'
,
`account_password` varchar(100) COLLATE
utf8_bin DEFAULT NULL COMMENT '帐户密码'
,`account_balance` double DEFAULT NULL COMMENT
'帐户余额'
,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT
CHARSET=utf8 COLLATE=utf8_bin
ROW_FORMAT=DYNAMIC;
/*Data for the table `account_info` */
insert into
`account_info`(`id`,`account_name`,`account_no`
,`account_password`,`account_balance`) values(2,'张三','1',NULL,1000);
bank2库,包含李四账户
CREATE DATABASE /*!32312 IF NOT EXISTS*/`bank2`
/*!40100 DEFAULT CHARACTER SET utf8 */;
USE `bank2`;
/*Table structure for table `account_info` */
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) COLLATE utf8_bin
DEFAULT NULL COMMENT '户主姓名'
,
`account_no` varchar(100) COLLATE utf8_bin
DEFAULT NULL COMMENT '银行卡号'
,
`account_password` varchar(100) COLLATE
utf8_bin DEFAULT NULL COMMENT '帐户密码'
,`account_balance` double DEFAULT NULL COMMENT
'帐户余额'
,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT
CHARSET=utf8 COLLATE=utf8_bin
ROW_FORMAT=DYNAMIC;
/*Data for the table `account_info` */
insert into
`account_info`(`id`,`account_name`,`account_no`
,`account_password`,`account_balance`) values(3,'李四','2',NULL,0);
Seata提供XA模式实现分布式事务_下载启动Seata服务
下载seata服务器
下载地址 :https://github.com/seata/seata/releases
解压并启动
tar -zxvf seata-server-1.4.2.tar.gz -C
/usr/local/
#后台运行
nohup sh seata-server.sh -p 9999 -h
192.168.66.100 -m file &> seata.log &
注意:
其中9999为服务端口号;file为启动模式,这里指seata服务将采用文件的方式存储信息。
测试
查看启动日志
cat seata.log
Seata提供XA模式实现分布式事务_搭建聚合父工程构建
创建工程distribute-transaction
字符编码
注解生效激活
Java编译版本选择
<!-- 指定JDK版本-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compilerplugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
File Type过滤
pom配置版本
<properties>
<spring-boot.version>2.6.3</spring-boot.version>
<spring.cloud.version>2021.0.1</spring.cloud.version>
<spring.cloud.alibaba.version>2021.0.1.0</spring.cloud.alibaba.version>
<lombok.version>1.18.22</lombok.version>
</properties>
<dependencyManagement>
<dependencies> <!--spring boot 2.6.3-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-bootstarter-parent</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-clouddependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringCloud Aliabab -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloudalibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
IDEA开启Dashboard
普通的Run面板
Run Dashboard面板
修改配置文件
在.idea/workspace.xml 文件中找到
添加配置
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule"/>
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType"/>
</set>
</option>
</component>
Seata提供XA模式实现分布式事务_转账功能实现上
实现如下功能
李四账户增加金额。
创建bank2
pom引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starterweb</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bootstarter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connectorjava</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starteralibaba-nacos-discovery</artifactId>
</dependency>
编写主启动类
//添加对mapper包扫描 Mybatis-plus
@MapperScan("com.itbaizhan.mapper")
@SpringBootApplication
@Slf4j
//开启发现注册
@EnableDiscoveryClient
public class SeataBank2Main6002 {
public static void main(String[] args) {
SpringApplication.run(SeataBank1Main6002.class,args);
log.info("************** SeataBank1Main6002 *************");
}
}
编写YML配置文件
server:
port: 6002
spring:
application:
name: seata-bank2
cloud:
nacos:
discovery:
# Nacos server地址
server-addr: 192.168.66.101:8848
datasource:
url: jdbc:mysql://localhost:3306/bank2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password01: 123456
driver-class-name: com.mysql.jdbc.Driver
代码生成
引入Mybatis Plus代码生成依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plusgenerator</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-enginecore</artifactId>
<version>2.0</version>
</dependency>
生成代码
package com.itbaizhan.utils;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.Arrays;
import java.util.List;
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://192.168.66.100:3306/bank2", "root", "123456")
.globalConfig(builder -> {
builder.author("itxiaotong")// 设置作者
.commentDate("MM-dd") // 注释日期格式
.outputDir(System.getProperty("user.dir")+"/xa-seata/bank2"+ "/src/main/java/") // 指定输出目录
.fileOverride(); //覆盖文件
})
// 包配置
.packageConfig(builder -> { builder.parent("com.itbaizhan") // 包名前缀
.entity("entity")//实体类包名
.mapper("mapper")//mapper接口包名
.service("service"); //service包名
})
.strategyConfig(builder -> {
// 设置需要生成的表名
List<String> strings = Arrays.asList("account_info");
builder.addInclude(strings)
// 开始实体类配置
.entityBuilder()
// 开启lombok模型
.enableLombok()
//表名下划线转驼峰
.naming(NamingStrategy.underline_to_camel)
//列名下划线转驼峰
.columnNaming(NamingStrategy.underline_to_camel);
})
.execute();
}
}
编写转账接口
public interface IAccountInfoService {
//李四增加金额
void updateAccountBalance(String accountNo, Double amount);
}
编写转账接口实现类
@Service
@Slf4j
public class AccountInfoServiceImpl implements IAccountInfoService {
@Autowired
AccountMapper accountMapper;
@Override
public void updateAccountBalance(String accountNo, Double amount) {
// 1. 获取用户信息
AccountInfo accountInfo = accountMapper.selectById(accountNo);
accountInfo.setAccountBalance(accountInfo.getAccountBalance() + amount);
accountMapper.updateById(accountInfo);
}
}
编写控制层
@RestController
@RequestMapping("/bank2")
public class Bank2Controller {
@Autowired
IAccountInfoService accountInfoService;
//接收张三的转账
@GetMapping("/transfer")
public String transfer(Double amount){
//李四增加金额
accountInfoService.updateAccountBalance("3",amount);
return "bank2"+amount;
}
}