在上一篇文章我们已经完成了商品不超卖的场景。但是我们可以看到,关于事务这块,这些服务其实用的都是同一个数据库实例,在实际的业务中,可能会存在有一个用户的数据库,存放在mysql实例A上,还有一个商品的数据库存放在mysql实例B上,还有一个订单的数据库存放在mysql实例C上。这时候依靠spring的事务就完成不了,因为可能A实例上的sql执行成功了,但是B实例的sql没有执行成功,此时A实例就要进行回滚。所以这里就要使用分布式事务了。这里我们比较推荐的分布式事务框架是Hmily,这个Hmily虽然用起来编写的代码比较多,但是整体逻辑很清晰,对于编码的人员来说能简单易懂。所以下面我们来演示下在这个秒杀系统里面添加Hmily框架。
一、分割数据库
在前面我们是把所有的表放在同一个库的,如下图:
这里我们模拟真实环节把表进行拆分,也就是把订单表,商品表,用户表存储在不同的实例上去。(这里我没有多的数据库实例,所以我们在一个数据库实例上进行演示,然后把表放在不同的库里面)。所以这里我们创建一个test11,test12,test13的库,然后把表信息导入进去:
这里这几个库对应的信息如下:
序号 | 库 | 说明 |
1 | test11 | 存放用户信息 |
2 | test12 | 存放商品信息 |
3 | test13 | 存放订单信息 |
同时这里我们也需要把user-service,goods-service,order-service的配置文件application.yml里面的jdbc信息修改下。
二、创建hmily数据库
这里使用hmily的话,也需要一个单独的数据库,可以和其他的数据库公用,这里我们创建一个名称为hmily的数据库
然后我们把hmily需要初始化的sql执行以下:
/* Navicat Premium Data Transfer Source Server : 演示数据库 Source Server Type : MySQL Source Server Version : 50738 Source Host : 192.168.31.30:3306 Source Schema : hmily Target Server Type : MySQL Target Server Version : 50738 File Encoding : 65001 Date: 04/07/2022 10:42:45 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for hmily_lock -- ---------------------------- DROP TABLE IF EXISTS `hmily_lock`; CREATE TABLE `hmily_lock` ( `lock_id` bigint(20) NOT NULL COMMENT '主键id', `trans_id` bigint(20) NOT NULL COMMENT '全局事务id', `participant_id` bigint(20) NOT NULL COMMENT 'hmily参与者id', `resource_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '资源id', `target_table_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '锁定目标表名', `target_table_pk` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '锁定表主键', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`lock_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'hmily全局lock表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for hmily_participant_undo -- ---------------------------- DROP TABLE IF EXISTS `hmily_participant_undo`; CREATE TABLE `hmily_participant_undo` ( `undo_id` bigint(20) NOT NULL COMMENT '主键id', `participant_id` bigint(20) NOT NULL COMMENT '参与者id', `trans_id` bigint(20) NOT NULL COMMENT '全局事务id', `resource_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '资源id,tac模式下为jdbc url', `undo_invocation` longblob NOT NULL COMMENT '回滚调用点', `status` tinyint(4) NOT NULL COMMENT '状态', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`undo_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'hmily事务参与者undo记录,用在AC模式' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for hmily_transaction_global -- ---------------------------- DROP TABLE IF EXISTS `hmily_transaction_global`; CREATE TABLE `hmily_transaction_global` ( `trans_id` bigint(20) NOT NULL COMMENT '全局事务id', `app_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名称', `status` tinyint(4) NOT NULL COMMENT '事务状态', `trans_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事务模式', `retry` int(11) NOT NULL DEFAULT 0 COMMENT '重试次数', `version` int(11) NOT NULL COMMENT '版本号', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`trans_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'hmily事务表(发起者)' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for hmily_transaction_participant -- ---------------------------- DROP TABLE IF EXISTS `hmily_transaction_participant`; CREATE TABLE `hmily_transaction_participant` ( `participant_id` bigint(20) NOT NULL COMMENT '参与者事务id', `participant_ref_id` bigint(20) NULL DEFAULT NULL COMMENT '参与者关联id且套调用时候会存在', `trans_id` bigint(20) NOT NULL COMMENT '全局事务id', `trans_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事务类型', `status` tinyint(4) NOT NULL COMMENT '分支事务状态', `app_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名称', `role` tinyint(4) NOT NULL COMMENT '事务角色', `retry` int(11) NOT NULL DEFAULT 0 COMMENT '重试次数', `target_class` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称', `target_method` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口方法名称', `confirm_method` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'confirm方法名称', `cancel_method` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'cancel方法名称', `confirm_invocation` longblob NULL COMMENT 'confirm调用点', `cancel_invocation` longblob NULL COMMENT 'cancel调用点', `version` int(11) NOT NULL DEFAULT 0, `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`participant_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'hmily事务参与者' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
执行完成之后,就可以在hmily这个库下面看到对应的4张表了。
备注:
1、这里的数据库名称只能用hmily,不能用其他的。
三、添加hmily框架
接着我们在order-service里面添加以下hmily的maven依赖
<dependency> <groupId>org.dromara</groupId> <artifactId>hmily-spring-boot-starter-springcloud</artifactId> <version>2.1.1</version> <exclusions> <exclusion> <groupId>org.dromara</groupId> <artifactId>hmily-config-apollo</artifactId> </exclusion> <exclusion> <groupId>org.dromara</groupId> <artifactId>hmily-repository-mongodb</artifactId> </exclusion> </exclusions> </dependency>
然后我们在src/main/resources目录下创建一个hmily.yml的文件,然后把如下的内容放进去:
hmily: server: configMode: local appName: order-service # 如果server.configMode eq local 的时候才会读取到这里的配置信息. config: appName: order-service serializer: kryo contextTransmittalMode: threadLocal scheduledThreadMax: 16 scheduledRecoveryDelay: 60 scheduledCleanDelay: 60 scheduledPhyDeletedDelay: 600 scheduledInitDelay: 30 recoverDelayTime: 60 cleanDelayTime: 180 limit: 200 retryMax: 10 bufferSize: 8192 consumerThreads: 16 asyncRepository: true autoSql: true phyDeleted: true storeDays: 3 repository: mysql remote: zookeeper: serverList: 192.168.31.10:2181 fileExtension: yml path: /hmily/order repository: database: driverClassName: com.mysql.jdbc.Driver url : jdbc:mysql://192.168.31.30:3306/hmily?useUnicode=true&characterEncoding=utf8 username: hmily password: N6TBxzCnxFYRfE7W maxActive: 20 minIdle: 10 connectionTimeout: 30000 idleTimeout: 600000 maxLifetime: 1800000
到这里我们为order-service就配置好了hmily框架。
四、添加hmily注解
到这里就是核心代码了,我们之前在下订单的service实现里面添加的是@Transactional注解,这是spring自带的,这里我们需要把它替换成@HmilyTCC。
然后这里我们使用的是tcc框架,也就是try-confirm-cancel结构,所以这里我们完整的代码如下:
@Override @HmilyTCC(confirmMethod = "doSeckillConfirmMethod", cancelMethod = "doSeckillCancelMethod") public BaseResponse doSeckill(String userNo, Long goodId) { // 首先我们判断下商品状态 这里略过,因为我们这里没有涉及到商品是否销售贺不销售等情况。 // 接着我们判断下当前商品是否是秒杀商品 BaseResponse res = goodsClient.getGoodsSeckillInfo(GoodRequest.builder().goodId(goodId).build()); GoodsSeckillDto seckillInfo = null; if (res.isOk()) { String info = JSON.toJSONString(res.getData()); seckillInfo = JSON.parseObject(info, GoodsSeckillDto.class); } if (null == seckillInfo) { return BaseResponse.fail(HttpStatusCode.GOODNOTSECKILL); } // 判断当前商品当前用户是否已经秒杀成功过了,如果有秒杀过,则返回不能再抢。这里由于我们一会要做并发测试,因此这里我们暂时不做此判断。 // 判断当前时间段是不是秒杀时间段 Long currentTime = new Date().getTime(); if (!(currentTime < seckillInfo.getEndDate().getTime() && currentTime > seckillInfo.getStartDate().getTime())) { return BaseResponse.fail(HttpStatusCode.GOODNOTSECKILLTIME); } // 判断库存 if (seckillInfo.getStockCount() < 1) { return BaseResponse.fail(HttpStatusCode.GOODSTOCKNOTENOUGH); } // 接着就可以下单了。 减库存->为用户增加未订单 Boolean success = goodsClient.reduceGoodStock(GoodRequest.builder().goodId(goodId).build()); if (!success) { return BaseResponse.fail(HttpStatusCode.GOODSTOCKNOTENOUGH); } // 获取用户信息 BaseResponse response = userClient.getUserInfoByNos(UserRequest.builder().userNos(userNo).build()); if (!response.isOk()) { return BaseResponse.fail(HttpStatusCode.ORDERFAILS); } // 创建订单 SeckillOrderDto newOrder = SeckillOrderDto.builder().userNo(userNo).orderId(IdWorker.getId()).goodsId(goodId) .build(); Boolean suc = odersManager.createNewSeckillOrder(newOrder); return suc ? BaseResponse.ok() : BaseResponse.fail(HttpStatusCode.ORDERFAILS); } /** * 秒杀确认方法,这里把预减的库存给他确认下。 */ public void doSeckillConfirmMethod(String userNo, Long goodId) { log.info("进入到了分布式事务确认方法"); goodsClient.confirmReduceGoodStockLog(SeckillStockRequest.builder() .userNo(userNo).goodId(goodId).build()); } /** * 秒杀取消方法 */ public void doSeckillCancelMethod(String userNo, Long goodId) { log.info("进入到了分布式事务取消方法"); // 如果执行错误的话,对应的逻辑是:把库存给扣了,新增了订单,把这两个信息反着做以下即可. // 首先还原库存 goodsClient.increGoodStock(SeckillStockRequest.builder() .userNo(userNo).goodId(goodId).build()); // 接着取消订单 this.cancelOrders(userNo, goodId); } @Transactional private void cancelOrders(String userNo, Long goodId) { odersManager.cancelOrders(userNo, goodId); }
这里我们制定了confirm方法,也指定了cancel方法,整体的逻辑如下:
1、在try方法里面扣库存,记录扣库存记录,新增订单。 2、在confirm阶段把扣库存记录标记为完结状态。 3、在cancel阶段还原库存,取消订单。
是不是很简单。然后我们测试以下,首先启动服务:
然后把库存改为10:
然后我们进行下秒杀:
可以看到秒杀成功了,然后我们分别查看对应的表:
1)首先查看库存表
库存少了1个没有问题。
2)接着我们查看库存扣记录表:
可以看到扣库存的记录表的状态status已经变成了3确认状态。
3)接着我们再查看生成的订单表:
已经生成了1张未支付的订单表。
然后我们再测试下分布式事务是否生效,还记得我们再订单表里面做了一个userno+goodid的唯一索引吗?
所以这里我们重复提交一个人的话,肯定是会报错的。所以我们再请求一下:
从这里我们可以看到,是从good-service报的错,但是我们的分布式事务是放在order-service的,所以我们接下来核对下数据表:
1)首先查看商品库存
商品库存还是9,没有任何问题。
2)接着我们查看扣库存的记录:
可以看到这里新增了2条记录,第一条是扣库存,第二条是把库存还原回去的。
3)接着我们看订单表:
这里没有生成新的订单。
基于上面的理解,我们可以看到分布式事务对于数据的控制是没有任何问题的,接着我们查看order-service,如下图:
可以看到,这里是由于报错,然后进入了hmily的cancel方法回退的库存。
以上就是在整个项目中添加分布式事务的实现。
备注:
1、这里我们新增的扣库存记录表的sql语句如下:
CREATE TABLE `reduce_stock_logs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `good_id` int(11) DEFAULT NULL COMMENT '商品id', `user_no` int(11) DEFAULT NULL COMMENT '用户编号', `status` tinyint(1) DEFAULT '1' COMMENT '库检日志,1、预减库存,2、回退库存,3、确认减库存', `reduce_id` int(11) DEFAULT '0' COMMENT '减库存对应的id', `cts` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4;
2、这里我们使用分布式事务的逻辑有一些瑕疵,大家能不能发现?我们将在下一篇文章中介绍。
最后按照惯例,附上本案例的源码,登陆后即可下载。
还没有评论,来说两句吧...