1, 背景
前段时间在看项目代码的时候, 发现有些接口的流程比较长, 在各个服务里面都有通过数据库事务保证数据的一致性, 但是在上游的 controller 层并没有对一致性做保证.
网上查了下, 还没找到基于 Go 开源的比较成熟的分布式事务框架.
于是, 准备看看之前隔壁部门大佬写的 tcc-transaction, 这是一个基于 tcc 思想实现的分布式事务框架.
tcc 分别代码 Try,Confirm 和 Cancel.
Try: 尝试执行业务
完成所有业务检查 (一致性)
预留必须业务资源 (准隔离性)
Confirm: 确认执行业务
真正执行业务
不作任何业务检查
只使用 Try 阶段预留的业务资源
Confirm 操作满足幂等性
Cancel: 取消执行业务
释放 Try 阶段预留的业务资源
Cancel 操作满足幂等性
要了解其实现原理, 第一步就是跑通项目自带的示例, 即 tcc-transaction-tutorial-sample 部分的代码.
今天主要介绍在跑通 tcc-transaction-tutorial-sample 过程中遇到的各种坑.
2, 依赖环境
- Java
- Maven
- Git
- MySQL
- Redis
- Zookeeper
- Intellij IDEA
源码地址: https://github.com/changmingxie/tcc-transaction
我自己 Fork 了一份 (配置改动已提交):https://github.com/DMinerJackie/tcc-transaction
3, 踩坑历程
踩坑准备
第一步: 克隆代码
使用 "git clone https://github.com/DMinerJackie/tcc-transaction" 命令下载代码
第二步: 导入代码并执行数据库脚本
代码导入 Intellij IDEA.
执行 tcc-transaction-http-sample/src/main/dbscripts 下的数据库脚本.
第三步: 修改配置文件
主要修改的是数据库配置参数. 拿 tcc-transaction-dubbo-sample 举例, 需要修改的文件有
tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-dubbo-sample/tcc-transaction-dubbo-capital/src/main/resources/tccjdbc.properties tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-dubbo-sample/tcc-transaction-dubbo-redpacket/src/main/resources/tccjdbc.properties tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-dubbo-sample/tcc-transaction-dubbo-order/src/main/resources/tccjdbc.properties
三个文件修改后对应配置如下
# 根据具体的 MySQL 版本使用驱动名称 jdbc.driverClassName=com.MySQL.cj.jdbc.Driver # 换成你连接数据库的地址 tcc.jdbc.url=jdbc:MySQL://127.0.0.1:3306/TCC?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false # 换成你需要配置数据库的用户名 jdbc.username=root # 换成你需要配置数据库的密码 jdbc.password=rootroot c3p0.initialPoolSize=10 c3p0.minPoolSize=10 c3p0.maxPoolSize=30 c3p0.acquireIncrement=3 c3p0.maxIdleTime=1800 c3p0.checkoutTimeout=30000
同时修改 tcc-transaction-sample-capital,tcc-transaction-sample-redpacket 和 tcc-transaction-sample-order 三个项目中 jdbc.proerties 文件的数据库连接, 修改后配置如下
jdbc.driverClassName=com.MySQL.jdbc.Driver jdbc.url=jdbc:MySQL://127.0.0.1:3306/TCC_CAP?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false tcc.jdbc.url=jdbc:MySQL://127.0.0.1:3306/TCC?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false jdbc.username=root jdbc.password=rootroot c3p0.initialPoolSize=10 c3p0.minPoolSize=10 c3p0.maxPoolSize=30 c3p0.acquireIncrement=3 c3p0.maxIdleTime=1800 c3p0.checkoutTimeout=30000
第四步: 启动项目
结合项目的 README.md 文件以及网上的文章了解到如果要跑通示例项目, 需要分别启动三个项目.
tcc-transaction 提供了两个版本:
基于 dubbo 通讯的示例版本
基于 http 通讯的示例版本
这两个版本对于的三个项目分别是
tcc-transaction-dubbo-capital(账户资产服务), tcc-transaction-dubbo-redpacket(红包服务), tcc-transaction-dubbo-order(交易订单服务) tcc-transaction-http-capital(账户资产服务), tcc-transaction-http-redpacket(红包服务), tcc-transaction-http-order(交易订单服务)
其实这两个版本我都跑过, 最终成功跑通的只有基于 dubbo 通讯的示例版本 (http 版本在最后 confirm 的时候最是失败, 导致最终订单状态为 unkown).
以基于 dubbo 通讯的示例为例
tcc-transaction-dubbo-capital 的启动配置如下
tcc-transaction-dubbo-redpacket 的启动配置如下
tcc-transaction-dubbo-order 的启动配置如下
坑 1: 连接不上 zk
启动 tcc-transaction-dubbo-capital 项目, 报错信息如下
[sample-dubbo-capital]2019-08-31 17:48:05,312 INFO [org.apache.zookeeper.ZooKeeper] Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=30000 watcher=org.I0Itec.zkclient.ZkClient@32c7bb63 [sample-dubbo-capital]2019-08-31 17:48:05,334 INFO [org.apache.zookeeper.ClientCnxn] Opening socket connection to server 127.0.0.1/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error) [sample-dubbo-capital]2019-08-31 17:48:05,344 WARN [org.apache.zookeeper.ClientCnxn] Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect java.NET.ConnectException: Connection refused at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717) at org.apache.zookeeper.ClientCnxnSocketNIO.doTransport(ClientCnxnSocketNIO.java:361) at org.apache.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1081) [sample-dubbo-capital]2019-08-31 17:48:06,456 INFO [org.apache.zookeeper.ClientCnxn] Opening socket connection to server 127.0.0.1/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error) [sample-dubbo-capital]2019-08-31 17:48:06,459 WARN [org.apache.zookeeper.ClientCnxn] Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect java.NET.ConnectException: Connection refused at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717) at org.apache.zookeeper.ClientCnxnSocketNIO.doTransport(ClientCnxnSocketNIO.java:361) at org.apache.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1081) [sample-dubbo-capital]2019-08-31 17:48:07,566 INFO [org.apache.zookeeper.ClientCnxn] Opening socket connection to server 127.0.0.1/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error) [sample-dubbo-capital]2019-08-31 17:48:07,567 WARN [org.apache.zookeeper.ClientCnxn] Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect java.NET.ConnectException: Connection refused
从报错信息, 一眼就看出是连不上 zk 即 zookeeper.
这个很好理解, 因为本地没有安装 zk, 于是安装并通过 "./zkServer.sh start" 启动 zk
坑 2:Redis 连不上
启动 tcc-transaction-dubbo-order 报错部分信息如下:
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: org.mengyun.tcctransaction.sample.dubbo.order.service.PlaceOrderServiceImpl org.mengyun.tcctransaction.sample.dubbo.order.web.controller.OrderController.placeOrderService; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'placeOrderServiceImpl': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: org.mengyun.tcctransaction.sample.dubbo.order.service.PaymentServiceImpl org.mengyun.tcctransaction.sample.dubbo.order.service.PlaceOrderServiceImpl.paymentService; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'paymentServiceImpl': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: org.mengyun.tcctransaction.sample.dubbo.capital.API.CapitalTradeOrderService org.mengyun.tcctransaction.sample.dubbo.order.service.PaymentServiceImpl.capitalTradeOrderService; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'captialTradeOrderService': Post-processing of FactoryBean's singleton object failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name'compensableTransactionAspect'defined in class path resource [tcc-transaction.xml]: Cannot resolve reference to bean'transactionConfigurator'while setting bean property'transactionConfigurator'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name'transactionConfigurator': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.mengyun.tcctransaction.TransactionRepository org.mengyun.tcctransaction.spring.support.SpringTransactionConfigurator.transactionRepository; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name'transactionRepository' defined in file [/Users/jackie/workspace/tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-dubbo-sample/tcc-transaction-dubbo-order/target/tcc-transaction-dubbo-order-1.2.6/Web-INF/classes/config/spring/local/appcontext-service-tcc.xml]: Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are: PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'jedisPool' threw exception; nested exception is Redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:526) at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:295) ... 60 more
这个和上面的原因类似, 本地没有安装 Redis, 导致无法拿到 Redis 连接.
于是安装 Redis, 并使用 "redis-server" 启动 Redis.
坑 3:Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection
三个项目都启动后, 可以看到一个商品链接列表页, 但是在点击链接后无法跳转, 并且报错如下
Type Exception Report Message Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: Description The server encountered an unexpected condition that prevented it from fulfilling the request. Exception org.springframework.Web.util.NestedServletException: Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out. ### The error may exist in URL [jar:file:/Users/jackie/workspace/tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-http-sample/tcc-transaction-http-order/target/tcc-transaction-http-order-1.2.6/Web-INF/lib/tcc-transaction-sample-order-1.2.6.jar!/config/sqlmap/main/sample-product.xml] ### The error may involve org.mengyun.tcctransaction.sample.order.infrastructure.dao.ProductDao.findByShopId ### The error occurred while executing a query ### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out. org.springframework.Web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:965) org.springframework.Web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:844) javax.servlet.http.HttpServlet.service(HttpServlet.java:634) org.springframework.Web.servlet.FrameworkServlet.service(FrameworkServlet.java:829) javax.servlet.http.HttpServlet.service(HttpServlet.java:741) org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) org.springframework.Web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88) org.springframework.Web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:106) Root Cause org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out. ### The error may exist in URL [jar:file:/Users/jackie/workspace/tcc-transaction/tcc-transaction-tutorial-sample/tcc-transaction-http-sample/tcc-transaction-http-order/target/tcc-transaction-http-order-1.2.6/Web-INF/lib/tcc-transaction-sample-order-1.2.6.jar!/config/sqlmap/main/sample-product.xml]
根据错误信息, 排查是 MySQL 版本和数据库驱动版本不匹配.
本地的 MySQL 版本是 "8.0.11 MySQL Community Server - GPL", 但是 tcc-transaction 中对应的驱动版本是
<dependency> <groupId>MySQL</groupId> <artifactId>MySQL-connector-java</artifactId> <version>5.1.33</version> </dependency>
改为
<dependency> <groupId>MySQL</groupId> <artifactId>MySQL-connector-java</artifactId> <version>8.0.11</version> </dependency>
同时针对高版本, 需要在连接的 jdbc-url 后面加上 useSSL=false
坑 4:Loading class `com.MySQL.jdbc.Driver'. This is deprecated
启动 tcc-transaction-dubbo-redpacket 时, 在日志中看到一个警告 "Loading class `com.mysql.jdbc.Driver'. This is deprecated".
通过搜索, 发现是因为数据库驱动 com.MySQL.jdbc.Driver'已经被弃用了, 需要使用 com.MySQL.cj.jdbc.Driver, 于是修改 jdbc.proerties 的配置 (具体配置见上面), 启动正常.
踩完上面的坑后, 启动三个项目, 完整走完流程, 实现了一个基于分布式事务的商品购买行为, 具体过程如下图所示
4, 总结
运行示例项目的过程不算太顺利, 主要有一下几个原因吧
本地环境配置和项目提供的不一致, 导致走了很多弯路, 比如 MySQL 的版本.
缺少详细的跑示例项目的文档说明.
网上提供的资料比较粗略, 也比较陈旧, 文中能跑起来的步骤说明已经不适用现在的代码了.
所以, 在踩完这么多坑总结下, 避免后面的人走同样的弯路.
如果您觉得阅读本文对您有帮助, 请点一下 "推荐" 按钮, 您的 "推荐" 将是我最大的写作动力! 如果您想持续关注我的文章, 请扫描二维码, 关注 JackieZheng 的微信公众号, 我会将我的文章推送给您, 并和您一起分享我日常阅读过的优质文章.
<em><img src="https://images0.cnblogs.com/blog2015/619240/201505/162205410643708.jpg" alt="" /></em>
来源: http://www.tuicool.com/articles/7BvArqY