官方文档:https://seata.io/zh-cn/index.html
GitHub文档:https://github.com/seata/seata
微服务SpringCloudAlibaba——简介(1)
微服务SpringClout Alibaba——Nacos注册中心、配置中心和Nacos集群(2)
微服务SpringClout Alibaba——Sentinel流控、熔断、降级(3)
微服务SpringClout Alibaba——Seata分布式事务(4)
微服务SpringCloud——GateWay网关(5)
什么是分布式系统
部署在不同结点上的系统通过网络交互来完成协同工作的系统
比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
什么是事务
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。
一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。 隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。
持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
什么是本地事务
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。
什么是分布式事务
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务。
提示:以下是本篇文章正文内容,下面案例可供参考
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata术语:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
前提
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
整体机制 两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段: 提交异步化,非常快速地完成。 回滚通过一阶段的回滚日志进行反向补偿。
Seata会拦截“业务SQL”
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。
1.提交
如果未出现异常情况,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
2.回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务SQL",还原业务数据。
回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。
如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。
下载地址:https://github.com/seata/seata/releases/download/v1.2.0/seata-server-1.2.0.tar.gz
在Linux上进行安装,tar -zxvf seata-server-1.2.0.tar.gz /usr/local
进入/usr/local/seata/conf 修改file.conf文件:
主要修改:
1、store下的mode改成db
2、db改成自己的数据库
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.0:3306/seata"
user = "root"
password = "root"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
进入/usr/local/seata/conf 修改registry.conf文件:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.226.137:80"
namespace = "a7d2e95f-00d1-4c0e-a9c7-1d4a64399856"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "192.168.226.137:80"
namespace = "a7d2e95f-00d1-4c0e-a9c7-1d4a64399856"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
建库建表,在版本1.0之前,db_store.sql在\seata-server-0.9.0\seata\conf目录里面
在1.0及其之后sql在GitHub的https://github.com/seata/seata/blob/v1.2.0/script/server/db/mysql.sql里面
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
本文章的Seata注册给Nacos(Seata能注册:file 、nacos 、eureka、redis、zk、consul、etcd3、sofa)
service.vgroupMapping.fsp_tx_group=default
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.0:3306/seata?useUnicode=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
先启动Nacos,再启动Seata
进入/usr/local/seata/bin启动seata:
sh seata-server.sh -p 8091 -h 192.168.226.135
建立一个订单库用与测试:seata_ order
CREATE DATABASE seata_order;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`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:已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
建立 UNDO_LOG 表,用户发生错误时回滚的表:
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
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;
代码:
properties文件配置如下:
server.port=2001
spring.application.name=seata-order-service
spring.cloud.nacos.discovery.server-addr=192.168.226.137:80
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.0:3306/seata_order
spring.datasource.username=root
spring.datasource.password=lhx19960303
feign.hystrix.enabled=false
logging.level.io.seata=info
management.endpoints.web.exposure.include=*
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=csdn.dao
seata.enabled=true
seata.application-id=seata-order-service
seata.tx-service-group=fsp_tx_group
seata.config.type=nacos
seata.config.nacos.namespace=a7d2e95f-00d1-4c0e-a9c7-1d4a64399856
seata.config.nacos.server-addr=192.168.226.137:80
seata.config.nacos.group=SEATA_GROUP
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=192.168.226.137:80
seata.registry.nacos.namespace=a7d2e95f-00d1-4c0e-a9c7-1d4a64399856
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos
domain
/**
* @author 小懒虫
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements Serializable
{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
/**
* 订单状态:0:创建中;1:已完结
*/
private Integer status;
}
dao
/**
* @author 小懒虫
*/
@Mapper
public interface OrderDao {
void create(Order order);
}
mapper
<mapper namespace="csdn.dao.OrderDao">
<resultMap id="BaseResultMap" type="csdn.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
resultMap>
<insert id="create">
insert into t_order (user_id,product_id,count,money,status)
values (#{userId},#{productId},#{count},#{money},0);
insert>
mapper>
service
/**
* @author 小懒虫
*/
public interface OrderService {
void create(Order order);
}
serviceImpl
import csdn.dao.OrderDao;
import csdn.domain.Order;
import csdn.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author 小懒虫
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Override
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
controller
/**
* @author 小懒虫
*/
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order){
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
正常运行插入数据:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
接下来模仿业务代码报异常的时候的情景:
/**
* @author 小懒虫
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Override
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
int i = 1/0;
}
}
报错:
数据库成功插入数据:
当我们在该方法上加入**@GlobalTransactional**
/**
* @author 小懒虫
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
/**
* name :唯一就行
* rollbackFor = Exception.class表示对任意异常都进行回滚
**/
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
int i = 1/0;
}
}
当我们成功插入数据后,数据库数据并没有任何改变,记录都添加不进来,达到出异常,数据库回滚的效果。