前言
單體應用被拆分紅各個獨立的業務模塊後,就不得不要去面對分佈式事務,好在阿里已經開源分佈式事務組件Seata,雖還在迭代中,不免會有bug產生,但隨着社區發展及反饋,相信終究會愈來愈穩定,話很少說讓咱們開始吧。html
項目版本
spring-boot.version:2.2.5.RELEASE
spring-cloud.version:Hoxton.SR3
seata.version:1.2.0
前端
項目說明
項目模塊說明以下:
前端請求接口經由網關服務進行路由轉發後進入cloud-web模塊,經cloud-web模塊調用相應業務微服務模塊,執行業務邏輯後響應前端請求。
java
Seata服務端部署
1.下載Seata服務端部署文件
https://github.com/seata/seata/releases/download/v1.2.0/seata-server-1.2.0.zip
如嫌下載慢,可關注本文下方微信公衆號二維碼,關注後回覆「666」便可獲取開發經常使用工具包
2.解壓至本地目錄後,執行seata-server.bat腳本,過程當中無報錯則說明部署正常,Linux環境下操做相似不作展開說明
git
Seata客戶端集成
cloud-web
部分pom.xml,後續模塊引入seata依賴同樣,後續模塊再也不單獨說明github
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>2.2.0.RELEASE</version> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency>
application.properties ,後續模塊引入seata配置項大體同樣,根據業務模塊調整3個配置項便可,後續模塊再也不單獨說明web
# seata配置 seata.enabled=true #seata.excludes-for-auto-proxying=firstClassNameForExclude,secondClassNameForExclude seata.application-id=cloud-web seata.tx-service-group=cloud-web_tx_group seata.enable-auto-data-source-proxy=true seata.use-jdk-proxy=false seata.client.rm.async-commit-buffer-limit=1000 seata.client.rm.report-retry-count=5 seata.client.rm.table-meta-check-enable=false seata.client.rm.report-success-enable=false seata.client.rm.saga-branch-register-enable=false seata.client.rm.lock.retry-interval=10 seata.client.rm.lock.retry-times=30 seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=true seata.client.tm.commit-retry-count=5 seata.client.tm.rollback-retry-count=5 seata.client.tm.degrade-check=false seata.client.tm.degrade-check-allow-times=10 seata.client.tm.degrade-check-period=2000 seata.client.undo.data-validation=true seata.client.undo.log-serialization=jackson seata.client.undo.only-care-update-columns=true seata.client.undo.log-table=undo_log seata.client.log.exceptionRate=100 seata.service.vgroup-mapping.cloud-web_tx_group=default seata.service.grouplist.default=${cloud-web.seata.service.grouplist.default} seata.service.enable-degrade=false seata.service.disable-global-transaction=false seata.transport.shutdown.wait=3 seata.transport.thread-factory.boss-thread-prefix=NettyBoss seata.transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker seata.transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler seata.transport.thread-factory.share-boss-worker=false seata.transport.thread-factory.client-selector-thread-prefix=NettyClientSelector seata.transport.thread-factory.client-selector-thread-size=1 seata.transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread seata.transport.thread-factory.worker-thread-size=default seata.transport.thread-factory.boss-thread-size=1 seata.transport.type=TCP seata.transport.server=NIO seata.transport.heartbeat=true seata.transport.serialization=seata seata.transport.compressor=none seata.transport.enable-client-batch-send-request=true seata.config.type=file seata.registry.type=file
重點3個配置項須要調整下,其他保持默認
seata.application-id=cloud-web
seata.tx-service-group=cloud-web_tx_group
seata.service.vgroup-mapping.cloud-web_tx_group=default
spring
OrderController.javasql
@RestController @RequestMapping(value = "/order") public class OrderController { @Autowired OrderFacade orderFacade; @GlobalTransactional @GetMapping("/add") public String add(@RequestParam("cartId") Long cartId){ orderFacade.addOrder(cartId); return "OK"; } }
在須要開啓分佈式事務的方法上添加@GlobalTransactional註解便可數據庫
module-order
項目結構圖以下
OrderService.java
微信
@RestController public class OrderService implements OrderFacade { @Autowired private TbOrderMapper tbOrderMapper; @Autowired private CartFacade cartFacade; @Autowired private GoodsFacade goodsFacade; @Autowired private WalletFacade walletFacade; /** * <p > * 功能:新增訂單 * </p> * @param cartId 購物車ID * @author wuyubin * @date 2020年05月22日 * @return */ @Override public void addOrder(Long cartId) { CartDTO cart = cartFacade.getCartById(cartId); TbOrder order = new TbOrder(); order.setUserId(cart.getUserId()); order.setGoodsId(cart.getGoodsId()); order.setOrderNo(String.valueOf(System.currentTimeMillis())); order.setCreateTime(System.currentTimeMillis()); order.setUpdateTime(order.getCreateTime()); order.setIsDeleted(Byte.valueOf("0")); // 新增訂單 tbOrderMapper.insert(order); // 刪除購物車 cartFacade.deleteCartById(cartId); GoodsDTO goods = goodsFacade.getByGoodsId(cart.getGoodsId()); // 扣減庫存 goodsFacade.substractStock(goods.getId()); // 扣減金額 walletFacade.substractMoney(cart.getUserId(),goods.getMoney()); throw new RuntimeException(); }
module-cart
項目結構圖以下
CartService.java
@RestController public class CartService implements CartFacade { Logger LOGGER = LoggerFactory.getLogger(CartService.class); @Autowired private TbCartMapper tbCartMapper; /** * <p > * 功能:增長商品至購物車 * </p> * @param userId 用戶ID * @param goodsId 商品ID * @author wuyubin * @date 2020年05月22日 * @return */ @Override public String addCart(Long userId,Long goodsId) { TbCart cart = new TbCart(); cart.setUserId(userId); cart.setGoodsId(goodsId); cart.setCreateTime(System.currentTimeMillis()); cart.setUpdateTime(cart.getCreateTime()); cart.setIsDeleted(Byte.valueOf("0")); tbCartMapper.insert(cart); return null; } /** * <p > * 功能:獲取購物車信息 * </p> * @param cartId 購物車ID * @author wuyubin * @date 2020年05月22日 * @return */ @Override public CartDTO getCartById(Long cartId) { CartDTO cartDTO = null; TbCart cart = tbCartMapper.selectById(cartId); if (null != cart) { cartDTO = new CartDTO(); cartDTO.setUserId(cart.getUserId()); cartDTO.setGoodsId(cart.getGoodsId()); } return cartDTO; } /** * <p > * 功能:刪除購物車信息 * </p> * @param cartId 購物車ID * @author wuyubin * @date 2020年05月22日 * @return */ @Override public void deleteCartById(Long cartId) { tbCartMapper.deleteById(cartId); } }
module-goods
項目結構圖以下
GoodsService.java
@RestController public class GoodsService implements GoodsFacade { @Autowired private TbGoodsMapper tbGoodsMapper; /** * <p > * 功能:獲取商品信息 * </p> * @param goodsId 商品ID * @author wuyubin * @date 2020年05月22日 * @return */ @Override public GoodsDTO getByGoodsId(Long goodsId) { GoodsDTO goodsDTO = null; TbGoods goods = tbGoodsMapper.selectById(goodsId); if (null != goods) { goodsDTO = new GoodsDTO(); BeanUtils.copyProperties(goods,goodsDTO); } return goodsDTO; } /** * <p > * 功能:扣減商品庫存 * </p> * @param goodsId 商品ID * @author wuyubin * @date 2020年05月22日 * @return */ @Override public void substractStock(@RequestParam("goodsId") Long goodsId) { if (tbGoodsMapper.updateSubstractStockNumById(goodsId) != 1) { throw new RuntimeException("扣減庫存異常"); } } }
module-wallet
項目結構圖以下
WalletService.java
@RestController public class WalletService implements WalletFacade { @Autowired private TbWalletMapper tbWalletMapper; /** * <p > * 功能:扣減用戶錢包金額 * </p> * @param userId 用戶ID * @param money 金額 * @author wuyubin * @date 2020年05月22日 * @return */ @Override public void substractMoney(Long userId, BigDecimal money) { if (tbWalletMapper.updateSubstractMoney(userId,money) != 1) { throw new RuntimeException("用戶金額異常"); } } }
表結構說明
undo_log 表:seata依賴表
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 DEFAULT CHARSET=utf8;
tb_cart 表:購物車表
CREATE TABLE `tb_cart` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_id` bigint(20) NULL DEFAULT NULL COMMENT '用戶ID', `goods_id` bigint(20) NULL DEFAULT NULL COMMENT '商品ID', `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間戳', `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間戳', `is_deleted` tinyint(4) NULL DEFAULT 0 COMMENT '刪除標誌 0:未刪除;1:已刪除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '購物車表' ROW_FORMAT = Dynamic; -- 初始化數據 INSERT INTO `tb_cart` VALUES (1, 1, 1, 1590114829756, 1590114829756, 0);
tb_goods 表:商品表
CREATE TABLE `tb_goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名稱', `stock_num` bigint(20) NULL DEFAULT NULL COMMENT '商品庫存數量', `money` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品金額', `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間戳', `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間戳', `is_deleted` tinyint(4) NULL DEFAULT NULL COMMENT '刪除標誌 0:未刪除;1:已刪除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品表' ROW_FORMAT = Dynamic; -- 初始化數據 INSERT INTO `tb_goods` VALUES (1, '鍵盤', 100, 100.00, 1590132270000, 1590377130, 0);
tb_wallet 表:錢包表
CREATE TABLE `tb_wallet` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_id` bigint(20) NULL DEFAULT NULL COMMENT '用戶ID', `money` decimal(10, 2) NULL DEFAULT NULL COMMENT '金額', `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間戳', `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間戳', `is_deleted` tinyint(4) NULL DEFAULT NULL COMMENT '刪除標誌 0:未刪除;1:已刪除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '錢包表' ROW_FORMAT = Dynamic; -- 初始化數據 INSERT INTO `tb_wallet` VALUES (1, 1, 500.00, 1590132270000, 1590377130, 0);
tb_order 表:訂單表
CREATE TABLE `tb_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `order_no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '訂單編號', `user_id` bigint(20) NULL DEFAULT NULL COMMENT '用戶ID', `goods_id` bigint(20) NULL DEFAULT NULL COMMENT '商品ID', `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間', `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間', `is_deleted` tinyint(4) NULL DEFAULT 0 COMMENT '是否刪除 0:未刪除;1:已刪除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '訂單表' ROW_FORMAT = Dynamic;
全部服務啓動後,請求如下接口
http://localhost:9005/cloud-web/order/add?cartId=1
查看各服務模塊日誌,你會發現均有以下信息輸出,提示已回滾
由於在order模塊中addOrder方法下,我這邊人爲拋出一個運行時異常,看樣子事務已經生效了
咱們看下數據庫中數據是否已回滾正常,覈實後發現數據均以回滾
接下來咱們把order模塊中addOrder方法下將「throw new RuntimeException();」代碼塊註釋掉,重啓訂單模塊服務後再次訪問上述接口地址,發現訪問正常
查看各服務模塊日誌,你會發現均有以下信息輸出,提示已提交成功
咱們再一次覈實下數據表中的數據
購物車表已將原先記錄邏輯刪除
訂單表新增一條訂單記錄
商品表庫存數量已扣減1
錢包表金額已扣減100
最後咱們測試其中一個服務出現異常,驗證下事務是否回滾正常,咱們將購物車表邏輯刪除恢復正常,將商品表庫存改爲0,這時咱們再請求上述接口地址,發現返回異常了,咱們再覈實下數據,發現數據表中的數據均以回滾。好啦,SpringCloud集成分佈式事務Seata的示例就到這裏啦,後續有深刻的研究再分享出來。
參考資料
https://github.com/seata/seata
https://seata.io/zh-cn/docs/overview/what-is-seata.html
系列文章
SpringCloud系列之配置中心(Config)使用說明
SpringCloud系列之服務註冊發現(Eureka)應用篇