事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。 原子性(atomicity):个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。
@Transational 大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。
什么是事务?
独立操作的业务单元
1.2. 分布式事务典型场景
当下互联网发展如火如荼,绝大部分公司都进行了数据库拆分和服务化(SOA)。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
典型的分布式事务场景:
跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过一个相对比较复杂的业务,一个业务中同时操作了9个库。下图演示了一个服务同时操作2个库的情 况:
分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql:insert into user(id,name) values (1,"张三"),(2,"李四")。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。
### 1、seata 阿里分布式事务框架 2、消息队列 3、saga 4、XA
他们有一个共同点,都是“两阶段(2PC)”。“两阶段”是指完成整个分布式事务,划分成两个步骤完成。 实际上,这四种常见的分布式事务解决方案, 分别对应着分布式事务的四种模式:AT、TCC、Saga、XA; 四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出;每种模式都有它的适用场 景,同样每个模式也都诞生有各自的代表产品;而这些代表产品,可能就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC)。 今天,我们会分别来看4种模式(AT、TCC、Saga、XA)的分布式事务实现。在看具体实现之前,先讲下分布式事务的理论基础。 分布式事务理论基础 解决分布式事务,也有相应的规范和协议。分布式事务相关的协议有2PC、3PC。 由于三阶段提交协议3PC非常难实现,目前市面主流的分布式事务解决方案都是2PC协议。这就是文章开始提及的常见分布式事务解决方案里面,那些列举的都有一个共同点“两阶段”的内在原因。 有些文章分析2PC时,几乎都会用TCC两阶段的例子,第一阶段try,第二阶段完成confirm或 cancel。其实2PC并不是专为实现TCC设计的,2PC具有普适性——协议一样的存在,目前绝大多数分布式解决方案都是以两阶段提交协议2PC为基础的。 TCC(Try-Confirm-Cancel) 实际上是服务化的两阶段提交协议。
准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数 据文件) 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
1.同步阻塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。 倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。 2.单点 在 2PC 中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。 如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长 等),所以也无法顺利处理上一个事务。 3.数据不一致 Commit 事务过程中 Commit 请求/Rollback 请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback 请求,而其他参与者则正常收到执行了Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。 当参与者执行 Commit/Rollback 后会向协调者发送 Ack,然而协调者不论是否收到所有的参与者的 Ack,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成 功”。 4.环境可靠性依赖 协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络 中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后, 会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现 实环境来说太苛刻了。
AT 模式是一种无侵入的分布式事务解决方案。 阿里seata框架,实现了该模式。 在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。 AT 模式如何做到对业务的无侵入 : 一阶段: 在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据, 在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。 二阶段回滚: 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。 AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
\1. 侵入性比较强, 并且得自己实现相关事务控制逻辑 2.在整个过程基本没有锁,性能更强 TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择,但是我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作,「代码量上去了,业务复杂了,性能下跌了」。**
在 Seata 的架构中,一共有三个角色:
维护全局和分支事务的状态,驱动全局事务提交或回滚。**TM (Transaction Manager) - 事务管理器** 定义全局事务的范围:开始全局事务、提交或回滚全局事务。**RM (Resource Manager) - 资源管理器** 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
性能损耗
一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。
性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
全局锁
热点数据
相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。
回滚锁释放时间
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
Server端存储模式(store.mode)支持三种: file:(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认) db:(5.7+)高可用模式,全局事务会话信息通过db共享,相应性能差些
第一步安装虚拟机
输入
ip a
查看自己的ip地址
第二步FinallShell配置端口连接(安装jdk nacos)
#进入 cd /opt
##运行以下shell.sh脚本安装基本的环境
##运行以下shell.sh脚本安装基本的环境
#修改机器名
hostnamectl set-hostname $1#修改静态网络
addr=$2 #192.168.64.130
sed -i 's/dhcp/static/' /etc/sysconfig/network-scripts/ifcfg-ens33
echo "IPADDR=$addr" >> /etc/sysconfig/network-scripts/ifcfg-ens33
echo "NETMASK=255.255.255.0" /etc/sysconfig/network-scripts/ifcfg-ens33 #子网掩码
gw=`awk 'BEGIN{split("'"$addr"'",ips,".");print ips[1] "." ips[2] "." ips[3] "." 2 }'`
echo "GATEWAY=$gw" >> /etc/sysconfig/network-scripts/ifcfg-ens33 #网关
echo "DNS1=114.114.114.114" >> /etc/sysconfig/network-scripts/ifcfg-ens33
echo "DNS2=8.8.8.8" >> /etc/sysconfig/network-scripts/ifcfg-ens33
systemctl restart network #重启网络#绑定地址和名字
echo "$addr $1" >> /etc/hosts#关闭防火墙
systemctl stop firewalld
systemctl disable firewalld#安装vim和 wget
yum install -y vim wget#更换yum源
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak3
#从阿里云下载
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum clean all
yum makecache#创建软件安装文件夹
mkdir -p /opt/soft#配置JDK
mkdir -p /opt/soft/jdk180
jdkPath=`find /opt/ -name 'jdk*.tar.gz*'`
tar -zxf $jdkPath -C /opt/soft/jdk180 --strip-components 1 #解压到jdk180 去一层文件夹if [ ! $JAVA_HOME ]
then
echo 'export JAVA_HOME=/opt/soft/jdk180' >> /etc/profile
echo 'export CLASSPATH=.:%JAVA_HOME/lib/dt.jar:%JAVA_HOME/lib/tools.jar' >> /etc/profile
echo 'export PATH=$PATH:$JAVA_HOME/bin' >> /etc/profile
source /etc/profile
fi
#1 启动激活
#myseata是自定义主机名 #192.168.64.135 是自己的ip地址 source shell02.sh myseata 192.168.64.135
#2 安装mysql5.7
卸载原有的**mariadb**
#查询文件名 rpm -qa | grep mariadb #xxx 上步查询到的文件名 然后卸载 rpm -e --nodeps xxx
下载安装mysql 5.7
wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm yum -y install mysql57-community-release-el7-10.noarch.rpm rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 yum install mysql-server -y
文件授权
chown -R root:root /var/lib/mysql chown root /var/lib/mysql/
mysql 中文乱码处理
# 编辑 /etc/my.cnf vim /etc/my.cnf [mysqld] character-set-server=utf8 [client] default-character-set=utf8 [mysql] default-character-set=utf8
#保存退出 :wq #重启 service mysqld restart
修改**mysql** 登录密码 开放远程登录权限
mysql5.7 登录
#查看临时密码 (在opt目录下) grep "password" /var/log/mysqld.log #复制这个密码 等下用这个临时密码登录
#登录数据库 nU5TydlD__a是你的自己的临时密码 mysql -uroot -p nU5TydlD__a use mysql #3090_Cmok 你的密码 ALTER USER 'root'@'localhost' IDENTIFIED BY '3090_Cmok'; # 修改远程登录 GRANT ALL PRIVILEGES ON *.* TO root@"%" IDENTIFIED BY "3090_Cmok"; flush privileges; exit; #(如果登不上 显示密码错误走下面方法)
-----------------------------------------------------------------------------------
基本思路都是按照如下步骤解决: 通过 vi /etc/my.cnf进入my.cnf配置文件; 在[mysqld]底下,加入一行 skip-grant-tables,以跳过安全验证; systemctl restart mysqld重启MySQL; mysql -uroot -p登陆MySQL,输入任意密码进入; 输入use mysql;进入名为mysql的数据库; 输入UPDATE user SET password=PASSWORD("你自己的密码") WHERE user='root';进行密码修改; 输入quit;退出MySQL; 重复步骤1; 删除刚才加入到[mysqld]下面的 skip-grant-tables,退出vim,重启MySQL; mysql -uroot -p输入刚才设置的新密码,登陆MySQL。 然而这个办法尝试了多次,都是在步骤6进行完以后报错 ERROR 1054 (42S22): Unknown column 'password' in 'field list' 都快疯了!!!! 偶然的机会,找到了CSDN上一篇点赞量很少的文章,提到 原来是MySQL5.7的mysql数据库下已经没有password这个字段了,password字段改成了 authentication_string 有句小可爱特别想喊出来!!!! 按照这篇文章的说法,将步骤6中的命令改成了: update mysql.user set authentication_string=password('HJZ@bb1314') where user='root'; 重新操作一遍。一下子就成功了! PS!!!!!!!!!!!!!!!!!!!!!!!! 后面用新修改的密码登陆MySQL的时候,可以登陆,但进行一些操作的时候,有如下warning: ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement. 意思是说,你刚才修改的新密码其实是一个初始密码,这里还要求你要换个密码才能继续登陆。 这就跟手动部署LNMP环境(Alibaba Cloud Linux 2)“步骤六:配置MySQL”接上了。
#3 搭建单机 nacos
jps cd /opt/ ls tar -zxf nacos-server-1.4.2.tar.gz mv nacos soft/nacos8848 cd soft/nacos8848/conf/ vim application.properties #放开 spring db db db db 修改ip地址192.168.64.135这ip是我的用自己的 #账号 root 密码 3090_Cmok cd ../bin/ ls pwd #常在/etc/profile文件中修改环境变量 vim /etc/profile #添加到末尾 ============================= #nacos env export NACOS_HOME=/opt/soft/nacos8848 export PATH=$PATH:$NACOS_HOME/bin ============================= #:wq! cd ~ cd /opt/soft/nacos8848/bin/ #关闭集群模式 vim startup.sh #=============================# #修改下面文件 export MODE="standalone" #=============================# #:wq! source /etc/profile ----------------------------------------- #重新进入mysql5.7激活sql mysql -uroot -p 3090_Cmok show databases; #创建数据库1 create database mydemo; use mydemo; #新建表单 create table stocks(id int primary key not null auto_increment,shopid int not null,storenum; #创建数据库2 create database nacos; use nacos; #激活 source /opt/soft/nacos8848/conf/nacos-mysql.sql exit ----------------------------------------- #启动 sh startup.sh #打开浏览器 192.168.64.135:8848/nacos/#/login
第一步idea父工程配置pom
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.2.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
第二步idea子工程配置pom
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> </dependencies>
第三步配置yml
server: port: 8002 spring: application: name: stockmodule datasource: druid: url: jdbc:mysql://192.168.64.135:3306/mydemo username: root password: 3090_Cmok initial-size: 3 max-active: 30 min-idle: 3 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: select 1 test-on-borrow: true test-while-idle: false test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 30 filter: stat,wall connection-properties: druid.stat.mergeSql=true;druid.stat.slowSq1Millis=500 use-global-data-source-stat: true cloud: nacos: discovery: server-addr: 192.168.64.135:8848 username: nacos password: nacos namespace: public mybatis-plus: mapper-locations: mapper/*.xml
第四步配置domain层 mapper层 service层 ctl层
#domain层 @Data @AllArgsConstructor @NoArgsConstructor @Builder public class Stocks { // 注解自增的注解@TableId @TableId(type = IdType.AUTO) private Integer id; private Integer shopid; private Integer storenum; } #mapper层 #接口 //BaseMapper 是mybatisplus的接口 可以实现快速crud @Mapper public interface StockMapper extends BaseMapper<Stocks> { int dedectStock(Stocks stocks); } #实现类xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.kgc.mynacos02.stockmodule.mapper.StockMapper"> <update id="dedectStock" parameterType="com.kgc.mynacos02.stockmodule.domain.Stocks"> update stocks set storenum=storenum-#{storenum} where id=#{id} </update> </mapper> #service层 @Service public class StockService { @Resource private StockMapper stockMapper; //增加库存 @Transactional public void addStock(Stocks stocks){ stockMapper.insert(stocks); } //修改库存 @Transactional public void modStock(Stocks stocks){ stockMapper.dedectStock(stocks); } } #Controller层 @RestController @RequestMapping("/stock") public class StockCtl { @Resource private StockService service; //测试接口cc @GetMapping("/cc") public String cc(){ return "cc"; } //放给前端 增加商品和库存 @GetMapping(value = "/addStock") public String addStock(@RequestBody Stocks stocks){ service.addStock(stocks); return "SUCCESS"; } //给其他服务调用修改库存 @GetMapping(value = "/deduct/{shopid}/{num}") public String deduct(@PathVariable("shopid")Integer shopid,@PathVariable("num") Integer num){ Stocks sto = Stocks.builder().shopid(shopid).storenum(num).build(); service.modStock(sto); return "SUCCESS"; } }
第五步启动Application 使用postman (apipost)测试接口
使用postman测试!!!
成功!!!
#查看数据库mysql5.7 mysql -uroot -p3090_Cmok use mydemo; mysql> select * from stocks; +----+--------+----------+ | id | shopid | storenum | +----+--------+----------+ | 1 | 2 | 40 | +----+--------+----------+ 1 row in set (0.00 sec)
再次成功!!!