|
| 1 | +# springboot-seata-transaction |
| 2 | +### 概览 |
| 3 | +##### 1.整合seata的demo,此demo都配置好了,拉下来按照步骤,直接可以跑起来观察效果。 |
| 4 | + |
| 5 | +##### 2.自己项目整合Seata,主要步骤如下: |
| 6 | +- 1.[下载seata-server](https://github.com/seata/seata/releases),修改server配置 |
| 7 | +- 2.client端(你自己的项目),引入配置文件,修改配置文件(注意不要遗漏,可参考下方几个关键步骤) |
| 8 | +- 3.数据源代理设置 |
| 9 | +- 4.创建数据库表 |
| 10 | +- 5.启动注册中心,启动server,启动client |
| 11 | + |
| 12 | +##### 关于调用成环和seata-server HA,见最后部分 |
| 13 | + |
| 14 | +### 1.此demo技术选型及版本信息 |
| 15 | + |
| 16 | +注册中心:eureka |
| 17 | + |
| 18 | +服务间调用:feign |
| 19 | + |
| 20 | +持久层:mybatis |
| 21 | + |
| 22 | +数据库:mysql 5.7.20 |
| 23 | + |
| 24 | +Springboot:2.1.7.RELEASE |
| 25 | + |
| 26 | +Springcloud:Greenwich.SR2 |
| 27 | + |
| 28 | +jdk:1.8 |
| 29 | + |
| 30 | +seata:0.8 |
| 31 | + |
| 32 | +使用不同组件,配置情况不同,可参考其他sample; |
| 33 | + |
| 34 | +### 2.demo概况 |
| 35 | +demo分为四个项目,单独启动。 |
| 36 | + |
| 37 | +- eureka:作为注册中心 |
| 38 | +- order:订单服务,用户下单后,会创建一个订单添加在order数据库,同时会扣减库存storage,扣减账户account; |
| 39 | +- storage:库存服务,用户扣减库存; |
| 40 | +- account:账户服务,用于扣减账户余额; |
| 41 | + |
| 42 | +order服务关键代码如下: |
| 43 | +```java |
| 44 | + @Override |
| 45 | + @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) //此注解开启全局事务 |
| 46 | + public void create(Order order) { |
| 47 | + //本地方法 创建订单 |
| 48 | + orderDao.create(order); |
| 49 | + //远程方法 扣减库存 |
| 50 | + storageApi.decrease(order.getProductId(),order.getCount()); |
| 51 | + //远程方法 扣减账户余额 可在accountServiceImpl中模拟异常 |
| 52 | + accountApi.decrease(order.getUserId(),order.getMoney()); |
| 53 | + } |
| 54 | +``` |
| 55 | +### 3.使用步骤 |
| 56 | +- 1.拉取本demo代码 git clone xxxx; |
| 57 | +- 2.[下载seata-server](https://github.com/seata/seata/releases); |
| 58 | +- 3.执行每个项目下的建表语句,resource下xx.sql文件; |
| 59 | +- 4.seata相关建表语句见下文说明; |
| 60 | + |
| 61 | +### 4.seata server端配置信息修改 |
| 62 | +seata-server中,/conf目录下,有两个配置文件,需要结合自己的情况来修改: |
| 63 | + |
| 64 | +##### 1.file.conf |
| 65 | + |
| 66 | +里面有事务组配置,锁配置,事务日志存储等相关配置信息,由于此demo使用db存储事务信息,我们这里要修改store中的配置: |
| 67 | +```java |
| 68 | +## transaction log store |
| 69 | +store { |
| 70 | + ## store mode: file、db |
| 71 | + mode = "db" 修改这里,表明事务信息用db存储 |
| 72 | + |
| 73 | + ## file store 当mode=db时,此部分配置就不生效了,这是mode=file的配置 |
| 74 | + file { |
| 75 | + dir = "sessionStore" |
| 76 | + |
| 77 | + # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions |
| 78 | + max-branch-session-size = 16384 |
| 79 | + # globe session size , if exceeded throws exceptions |
| 80 | + max-global-session-size = 512 |
| 81 | + # file buffer size , if exceeded allocate new buffer |
| 82 | + file-write-buffer-cache-size = 16384 |
| 83 | + # when recover batch read size |
| 84 | + session.reload.read_size = 100 |
| 85 | + # async, sync |
| 86 | + flush-disk-mode = async |
| 87 | + } |
| 88 | + |
| 89 | + ## database store mode=db时,事务日志存储会存储在这个配置的数据库里 |
| 90 | + db { |
| 91 | + ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. |
| 92 | + datasource = "dbcp" |
| 93 | + ## mysql/oracle/h2/oceanbase etc. |
| 94 | + db-type = "mysql" |
| 95 | + driver-class-name = "com.mysql.jdbc.Driver" |
| 96 | + url = "jdbc:mysql://116.62.62.26/seat-server" 修改这里 |
| 97 | + user = "root" 修改这里 |
| 98 | + password = "root" 修改这里 |
| 99 | + min-conn = 1 |
| 100 | + max-conn = 3 |
| 101 | + global.table = "global_table" |
| 102 | + branch.table = "branch_table" |
| 103 | + lock-table = "lock_table" |
| 104 | + query-limit = 100 |
| 105 | + } |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +由于此demo我们使用db模式存储事务日志,所以,我们要创建三张表:global_table,branch_table,lock_table,建表sql在seata/seata/script/server/db/mysql.sql; |
| 110 | + |
| 111 | +由于存储undo_log是在业务库中,所以在每个业务库中,还要创建undo_log表,建表sql在/conf/db_undo_log.sql中。 |
| 112 | + |
| 113 | +由于我自定义了事务组名称,所以这里也做了修改: |
| 114 | +```java |
| 115 | +service { |
| 116 | + #vgroup->rgroup |
| 117 | + vgroup_mapping.fsp_tx_group = "default" 修改这里,fsp_tx_group这个事务组名称是我自定义的,一定要与client端的这个配置一致!否则会报错! |
| 118 | + #only support single node |
| 119 | + default.grouplist = "127.0.0.1:8091" 此配置作用参考:https://blog.csdn.net/weixin_39800144/article/details/100726116 |
| 120 | + #degrade current not support |
| 121 | + enableDegrade = false |
| 122 | + #disable |
| 123 | + disable = false |
| 124 | + #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent |
| 125 | + max.commit.retry.timeout = "-1" |
| 126 | + max.rollback.retry.timeout = "-1" |
| 127 | +} |
| 128 | +``` |
| 129 | +其他的可以先使用默认值。 |
| 130 | + |
| 131 | +##### 2.registry.conf |
| 132 | + |
| 133 | +registry{}中是注册中心相关配置,config{}中是配置中心相关配置。seata中,注册中心和配置中心是分开实现的,是两个东西。 |
| 134 | + |
| 135 | +我们这里用eureka作注册中心,所以,只用修改registry{}中的: |
| 136 | +```java |
| 137 | +registry { |
| 138 | + # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa |
| 139 | + type = "eureka" 修改这里,指明注册中心使用什么 |
| 140 | + |
| 141 | + nacos { |
| 142 | + serverAddr = "localhost" |
| 143 | + namespace = "" |
| 144 | + cluster = "default" |
| 145 | + } |
| 146 | + eureka { |
| 147 | + serviceUrl = "http://localhost:8761/eureka" 修改这里 |
| 148 | + application = "default" |
| 149 | + weight = "1" |
| 150 | + } |
| 151 | + redis { |
| 152 | + serverAddr = "localhost:6379" |
| 153 | + db = "0" |
| 154 | + } |
| 155 | + zk { |
| 156 | + cluster = "default" |
| 157 | + serverAddr = "127.0.0.1:2181" |
| 158 | + session.timeout = 6000 |
| 159 | + connect.timeout = 2000 |
| 160 | + } |
| 161 | + consul { |
| 162 | + cluster = "default" |
| 163 | + serverAddr = "127.0.0.1:8500" |
| 164 | + } |
| 165 | + etcd3 { |
| 166 | + cluster = "default" |
| 167 | + serverAddr = "http://localhost:2379" |
| 168 | + } |
| 169 | + sofa { |
| 170 | + serverAddr = "127.0.0.1:9603" |
| 171 | + application = "default" |
| 172 | + region = "DEFAULT_ZONE" |
| 173 | + datacenter = "DefaultDataCenter" |
| 174 | + cluster = "default" |
| 175 | + group = "SEATA_GROUP" |
| 176 | + addressWaitTime = "3000" |
| 177 | + } |
| 178 | + file { |
| 179 | + name = "file.conf" |
| 180 | + } |
| 181 | +} |
| 182 | +``` |
| 183 | +其他的配置可以暂时使用默认值。 |
| 184 | + |
| 185 | +如果是在windows下启动seata-server,现在已经完成配置修改了,等eureka启动后,就可以启动seata-server了:执行/bin/seata-server.bat即可。 |
| 186 | + |
| 187 | +### 5.client端相关配置 |
| 188 | +#### 1.普通配置 |
| 189 | +client端的几个服务,都是普通的springboot整合了springCloud组件的正常服务,所以,你需要配置eureka,数据库,mapper扫描等,即使不使用seata,你也需要做,这里不做特殊说明,看代码就好。 |
| 190 | + |
| 191 | +#### 2.特殊配置 |
| 192 | +##### 1.application.yml |
| 193 | +以order服务为例,除了常规配置外,这里还要配置下事务组信息: |
| 194 | +```java |
| 195 | +spring: |
| 196 | + application: |
| 197 | + name: order-server |
| 198 | + cloud: |
| 199 | + alibaba: |
| 200 | + seata: |
| 201 | + tx-service-group: fsp_tx_group 这个fsp_tx_group自定义命名很重要,server,client都要保持一致 |
| 202 | +``` |
| 203 | +##### 2.file.conf |
| 204 | +自己新建的项目是没有这个配置文件的,copy过来,修改下面配置: |
| 205 | +```java |
| 206 | +service { |
| 207 | + #vgroup->rgroup |
| 208 | + vgroup_mapping.fsp_tx_group = "default" 这个fsp_tx_group自定义命名很重要,server,client都要保持一致 |
| 209 | + #only support single node |
| 210 | + default.grouplist = "127.0.0.1:8091" |
| 211 | + #degrade current not support |
| 212 | + enableDegrade = false |
| 213 | + #disable |
| 214 | + disable = false |
| 215 | + disableGlobalTransaction = false |
| 216 | +} |
| 217 | +``` |
| 218 | +##### 3.registry.conf |
| 219 | + |
| 220 | +使用eureka做注册中心,仅需要修改eureka的配置即可: |
| 221 | +```java |
| 222 | +registry { |
| 223 | + # file 、nacos 、eureka、redis、zk |
| 224 | + type = "eureka" 修改这里 |
| 225 | + |
| 226 | + nacos { |
| 227 | + serverAddr = "localhost" |
| 228 | + namespace = "public" |
| 229 | + cluster = "default" |
| 230 | + } |
| 231 | + eureka { |
| 232 | + serviceUrl = "http://localhost:8761/eureka" 修改这里 |
| 233 | + application = "default" |
| 234 | + weight = "1" |
| 235 | + } |
| 236 | + redis { |
| 237 | + serverAddr = "localhost:6381" |
| 238 | + db = "0" |
| 239 | + } |
| 240 | + zk { |
| 241 | + cluster = "default" |
| 242 | + serverAddr = "127.0.0.1:2181" |
| 243 | + session.timeout = 6000 |
| 244 | + connect.timeout = 2000 |
| 245 | + } |
| 246 | + file { |
| 247 | + name = "file.conf" |
| 248 | + } |
| 249 | +} |
| 250 | +``` |
| 251 | +其他的使用默认值就好。 |
| 252 | + |
| 253 | +#### 3.数据源代理 |
| 254 | +这个是要特别注意的地方,seata对数据源做了代理和接管,在每个参与分布式事务的服务中,都要做如下配置: |
| 255 | +```java |
| 256 | +/** |
| 257 | + * 数据源代理 |
| 258 | + * @author wangzhongxiang |
| 259 | + */ |
| 260 | +@Configuration |
| 261 | +public class DataSourceConfiguration { |
| 262 | + |
| 263 | + @Bean |
| 264 | + @ConfigurationProperties(prefix = "spring.datasource") |
| 265 | + public DataSource druidDataSource(){ |
| 266 | + DruidDataSource druidDataSource = new DruidDataSource(); |
| 267 | + return druidDataSource; |
| 268 | + } |
| 269 | + |
| 270 | + @Primary |
| 271 | + @Bean("dataSource") |
| 272 | + public DataSourceProxy dataSource(DataSource druidDataSource){ |
| 273 | + return new DataSourceProxy(druidDataSource); |
| 274 | + } |
| 275 | + |
| 276 | + @Bean |
| 277 | + public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{ |
| 278 | + SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); |
| 279 | + sqlSessionFactoryBean.setDataSource(dataSourceProxy); |
| 280 | + sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() |
| 281 | + .getResources("classpath*:/mapper/*.xml")); |
| 282 | + sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); |
| 283 | + return sqlSessionFactoryBean.getObject(); |
| 284 | + } |
| 285 | + |
| 286 | +} |
| 287 | +``` |
| 288 | + |
| 289 | +### 6.启动测试 |
| 290 | +- 1.启动eureka; |
| 291 | +- 2.启动seata-server; |
| 292 | +- 3.启动order,storage,account服务; |
| 293 | +- 4.访问:http://localhost:8180/order/create?userId=1&productId=1&count=10&money=100 |
| 294 | + |
| 295 | +然后可以模拟正常情况,异常情况,超时情况等,观察数据库即可。 |
| 296 | + |
| 297 | +这个demo,未做各种优化,如果压测,需要修改和优化一些配置,压测出错了,不一定是seata的锅,自己先排查,再去群里问问。 |
| 298 | + |
| 299 | +### 7.日志 |
| 300 | +正常情况: |
| 301 | +##### 1.order |
| 302 | +```java |
| 303 | +2019-09-06 15:44:33.536 INFO 53904 --- [io-8080-exec-10] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.158.133:8091:2021468859] |
| 304 | +2019-09-06 15:44:33.536 INFO 53904 --- [io-8080-exec-10] c.j.order.service.OrderServiceImpl : ------->交易开始 |
| 305 | +2019-09-06 15:44:34.376 INFO 53904 --- [io-8080-exec-10] c.j.order.service.OrderServiceImpl : ------->交易结束 |
| 306 | +2019-09-06 15:44:34.593 INFO 53904 --- [io-8080-exec-10] i.seata.tm.api.DefaultGlobalTransaction : [192.168.158.133:8091:2021468859] commit status:Committed |
| 307 | +2019-09-06 15:44:35.296 INFO 53904 --- [atch_RMROLE_6_8] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.158.133:8091:2021468859,branchId=2021468861,branchType=AT,resourceId=jdbc:mysql://116.62.62.26/seat-order,applicationData=null |
| 308 | +2019-09-06 15:44:35.297 INFO 53904 --- [atch_RMROLE_6_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.158.133:8091:2021468859 2021468861 jdbc:mysql://116.62.62.26/seat-order null |
| 309 | +2019-09-06 15:44:35.297 INFO 53904 --- [atch_RMROLE_6_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed |
| 310 | +``` |
| 311 | +##### 2.storage |
| 312 | +```java |
| 313 | +2019-09-06 15:44:33.776 INFO 9704 --- [nio-8082-exec-1] c.j.storage.service.StorageServiceImpl : ------->扣减库存开始 |
| 314 | +2019-09-06 15:44:34.030 INFO 9704 --- [nio-8082-exec-1] c.j.storage.service.StorageServiceImpl : ------->扣减库存结束 |
| 315 | +2019-09-06 15:44:35.422 INFO 9704 --- [atch_RMROLE_5_8] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.158.133:8091:2021468859,branchId=2021468864,branchType=AT,resourceId=jdbc:mysql://116.62.62.26/seat-storage,applicationData=null |
| 316 | +2019-09-06 15:44:35.423 INFO 9704 --- [atch_RMROLE_5_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.158.133:8091:2021468859 2021468864 jdbc:mysql://116.62.62.26/seat-storage null |
| 317 | +2019-09-06 15:44:35.423 INFO 9704 --- [atch_RMROLE_5_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed |
| 318 | +``` |
| 319 | + |
| 320 | +##### 3.account |
| 321 | +```java |
| 322 | +2019-09-06 15:44:34.039 INFO 36556 --- [nio-8081-exec-5] c.j.account.service.AccountServiceImpl : ------->扣减账户开始 |
| 323 | +2019-09-06 15:44:34.039 INFO 36556 --- [nio-8081-exec-5] c.j.account.service.AccountServiceImpl : ------->扣减账户结束 |
| 324 | +2019-09-06 15:44:35.545 INFO 36556 --- [atch_RMROLE_3_8] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.158.133:8091:2021468859,branchId=2021468867,branchType=AT,resourceId=jdbc:mysql://116.62.62.26/seat-account,applicationData=null |
| 325 | +2019-09-06 15:44:35.545 INFO 36556 --- [atch_RMROLE_3_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.158.133:8091:2021468859 2021468867 jdbc:mysql://116.62.62.26/seat-account null |
| 326 | +2019-09-06 15:44:35.545 INFO 36556 --- [atch_RMROLE_3_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed |
| 327 | +``` |
| 328 | +### 8.模拟异常 |
| 329 | +在AccountServiceImpl中模拟异常情况,然后可以查看日志 |
| 330 | +```java |
| 331 | + /** |
| 332 | + * 扣减账户余额 |
| 333 | + * @param userId 用户id |
| 334 | + * @param money 金额 |
| 335 | + */ |
| 336 | + @Override |
| 337 | + public void decrease(Long userId, BigDecimal money) { |
| 338 | + LOGGER.info("------->扣减账户开始"); |
| 339 | +// try { |
| 340 | +// Thread.sleep(30*1000); |
| 341 | +// } catch (InterruptedException e) { |
| 342 | +// e.printStackTrace(); |
| 343 | +// } |
| 344 | + LOGGER.info("------->扣减账户结束"); |
| 345 | + accountDao.decrease(userId,money); |
| 346 | + } |
| 347 | +``` |
| 348 | +### 9.调用成环 |
| 349 | +前面的调用链为order->storage->account; |
| 350 | +这里测试的成环是指order->storage->account->order, |
| 351 | +这里的account服务又会回头去修改order在前面添加的数据。 |
| 352 | +经过测试,是支持此种场景的。 |
| 353 | +```java |
| 354 | + /** |
| 355 | + * 扣减账户余额 |
| 356 | + * @param userId 用户id |
| 357 | + * @param money 金额 |
| 358 | + */ |
| 359 | + @Override |
| 360 | + public void decrease(Long userId, BigDecimal money) { |
| 361 | + LOGGER.info("------->扣减账户开始account中"); |
| 362 | + //模拟超时异常,全局事务回滚 |
| 363 | +// try { |
| 364 | +// Thread.sleep(30*1000); |
| 365 | +// } catch (InterruptedException e) { |
| 366 | +// e.printStackTrace(); |
| 367 | +// } |
| 368 | + accountDao.decrease(userId,money); |
| 369 | + LOGGER.info("------->扣减账户结束account中"); |
| 370 | + |
| 371 | + //修改订单状态,此调用会导致调用成环 |
| 372 | + LOGGER.info("修改订单状态开始"); |
| 373 | + String mes = orderApi.update(userId, money.multiply(new BigDecimal("0.09")),0); |
| 374 | + LOGGER.info("修改订单状态结束:{}",mes); |
| 375 | + } |
| 376 | +``` |
| 377 | +在最初的order会创建一个订单,然后扣减库存,然后扣减账户,账户扣减完,会回头修改订单的金额和状态,这样调用就成环了。 |
| 378 | + |
| 379 | +### 10.seata-server HA |
| 380 | +下载seata server包,地址:https://github.com/seata/seata/releases; |
| 381 | + |
| 382 | +部署集群,第一台和第二台配置相同,在server端的registry.conf中,注意: |
| 383 | +```java |
| 384 | +registry { |
| 385 | + # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa |
| 386 | + type = "eureka" |
| 387 | +...... |
| 388 | + eureka { |
| 389 | + serviceUrl = "http://192.168.xx.xx:8761/eureka" //两台tcc相同,注册中心的地址 |
| 390 | + application = "default" //两台tc相同 |
| 391 | + weight = "1" //权重,截至0.9版本,暂时不支持此参数 |
| 392 | + } |
| 393 | + ...... |
| 394 | +``` |
| 395 | +注意上述配置和client的配置要一致,2台和多台情况相同。 |
| 396 | + |
| 397 | +0.9及之前版本,多tc时,tc会误报异常,此问题0.9之后已经修复,之后的版本应该不会出现此问题。 |
0 commit comments