:::tips 这里通过一个案例来进行实现TCC模式 :::

创建数据表

  1. CREATE TABLE `account_freeze_tbl` (
  2. `xid` varchar(128) NOT NULL,
  3. `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  4. `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  5. `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  6. PRIMARY KEY (`xid`) USING BTREE
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

:::tips

  • xid:是全局事务id
  • freeze_money:用来记录用户冻结金额
  • state:用来记录事务状态

业务流程

  • Try业务:
    • 记录冻结金额和事务状态到account_freeze表
    • 扣减account表可用金额
  • Confirm业务
    • 根据xid删除account_freeze表的冻结记录
  • Cancel业务
    • 修改account_freeze表,冻结金额为0,state为2
    • 修改account表,恢复可用金额
  • 判断是否空回滚
    • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
  • 避免业务悬挂

    • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务 :::

      声明TCC接口

      :::tips TCC模式的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,在微服务工程中的service包中新建一个接口,用来声明TCC模式的三个接口 :::

      1. /**
      2. * @version 1.0
      3. * @description 说明
      4. * @package cn.itcast.account.service
      5. */
      6. @LocalTCC
      7. public interface AccountTCCService {
      8. /**
      9. * 用户账户扣款
      10. * 1 扣减account表可用金额
      11. * 2 记录冻结金额和事务状态到account_freeze表
      12. * @param userId
      13. * @param money
      14. *
      15. */
      16. @TwoPhaseBusinessAction(name = "deduct",
      17. commitMethod = "confirm",rollbackMethod = "cancel")
      18. void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
      19. @BusinessActionContextParameter(paramName = "money") int money);
      20. /**
      21. * 确认业务,根据xid删除account_freeze表的冻结记录
      22. * @param ctx
      23. * @return
      24. */
      25. boolean confirm(BusinessActionContext ctx);
      26. /**
      27. * 撤消回滚,需要把冻结金额还原
      28. * 1 修改account_freeze表,冻结金额为0,state为2
      29. * 2 修改account表,恢复可用金额
      30. * @param ctx
      31. * @return
      32. */
      33. boolean cancel(BusinessActionContext ctx);
      34. }

      实现接口

      :::tips 在impl包中新建一个类,实现刚刚编写的接口 :::

      1. /**
      2. * @version 1.0
      3. * @description 说明
      4. * @package cn.itcast.account.service.impl
      5. */
      6. @Service
      7. @Slf4j
      8. public class AccountTCCServiceImpl implements AccountTCCService {
      9. @Autowired
      10. private AccountMapper accountMapper;
      11. @Autowired
      12. private AccountFreezeMapper accountFreezeMapper;
      13. /**
      14. * 用户账户扣款
      15. * 1 扣减account表可用金额
      16. * 2 记录冻结金额和事务状态到account_freeze表
      17. * @param userId
      18. * @param money
      19. *
      20. */
      21. @Override
      22. @Transactional
      23. public void deduct(String userId, int money) {
      24. String xid = RootContext.getXID();
      25. //1. 扣减金额
      26. accountMapper.deduct(userId,money);
      27. log.debug("扣减金额成功: {},userId={}", xid, userId);
      28. //2. 添加冻结记录
      29. AccountFreeze freeze = new AccountFreeze();
      30. freeze.setFreezeMoney(money);
      31. freeze.setState(AccountFreeze.State.TRY);
      32. freeze.setXid(xid);
      33. freeze.setUserId(userId);
      34. accountFreezeMapper.insert(freeze);
      35. log.debug("冻结金额成功! {},userId={}", xid, userId);
      36. //3. 添加本地事务控制
      37. }
      38. /**
      39. * 确认业务,根据xid删除account_freeze表的冻结记录
      40. * @param ctx
      41. * @return
      42. */
      43. @Override
      44. public boolean confirm(BusinessActionContext ctx) {
      45. //1. 获取事务Id
      46. String xid = ctx.getXid();
      47. log.debug("进入confirm: {}", xid);
      48. //2. 通过事务Id删除数据
      49. int count = accountFreezeMapper.deleteById(xid);
      50. log.debug("删除冻结记录 {},count={}",xid,count);
      51. return 1==count;
      52. }
      53. /**
      54. * 撤消回滚,需要把冻结金额还原
      55. * 1 修改account_freeze表,冻结金额为0,state为2
      56. * 2 修改account表,恢复可用金额
      57. * @param ctx
      58. * @return
      59. */
      60. @Override
      61. @Transactional
      62. public boolean cancel(BusinessActionContext ctx) {
      63. //1. 获取事务Id
      64. String xid = ctx.getXid();
      65. //2. 查询冻结记录
      66. AccountFreeze freeze = accountFreezeMapper.selectById(xid);
      67. //3. 回滚金额
      68. String userId = ctx.getActionContext("userId").toString();
      69. log.debug("开始回滚金额: {},userId={}", xid, userId);
      70. accountMapper.refund(userId, freeze.getFreezeMoney());
      71. //4. 更新冻结金额为0,且更新状态为cancel
      72. freeze = new AccountFreeze();
      73. freeze.setFreezeMoney(0);
      74. freeze.setState(AccountFreeze.State.CANCEL);
      75. freeze.setXid(xid);
      76. int count = accountFreezeMapper.updateById(freeze);
      77. log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
      78. //5. 本地事务控制
      79. return count==1;
      80. }
      81. }

      解决空回滚

      :::tips 在Cancel方法中,根据xid查询account_freeze,如果为null则说明Try还没做,需要空回滚 :::

      1. /**
      2. * @version 1.0
      3. * @description 说明
      4. * @package cn.itcast.account.service.impl
      5. */
      6. @Service
      7. @Slf4j
      8. public class AccountTCCServiceImpl implements AccountTCCService {
      9. @Autowired
      10. private AccountMapper accountMapper;
      11. @Autowired
      12. private AccountFreezeMapper accountFreezeMapper;
      13. /**
      14. * 用户账户扣款
      15. * 1 扣减account表可用金额
      16. * 2 记录冻结金额和事务状态到account_freeze表
      17. * @param userId
      18. * @param money
      19. *
      20. */
      21. @Override
      22. @Transactional
      23. public void deduct(String userId, int money) {
      24. String xid = RootContext.getXID();
      25. //1. 扣减金额
      26. accountMapper.deduct(userId,money);
      27. log.debug("扣减金额成功: {},userId={}", xid, userId);
      28. //2. 添加冻结记录
      29. AccountFreeze freeze = new AccountFreeze();
      30. freeze.setFreezeMoney(money);
      31. freeze.setState(AccountFreeze.State.TRY);
      32. freeze.setXid(xid);
      33. freeze.setUserId(userId);
      34. accountFreezeMapper.insert(freeze);
      35. log.debug("冻结金额成功! {},userId={}", xid, userId);
      36. //3. 添加本地事务控制
      37. }
      38. /**
      39. * 确认业务,根据xid删除account_freeze表的冻结记录
      40. * @param ctx
      41. * @return
      42. */
      43. @Override
      44. public boolean confirm(BusinessActionContext ctx) {
      45. //1. 获取事务Id
      46. String xid = ctx.getXid();
      47. log.debug("进入confirm: {}", xid);
      48. //2. 通过事务Id删除数据
      49. int count = accountFreezeMapper.deleteById(xid);
      50. log.debug("删除冻结记录 {},count={}",xid,count);
      51. return 1==count;
      52. }
      53. /**
      54. * 撤消回滚,需要把冻结金额还原
      55. * 1 修改account_freeze表,冻结金额为0,state为2
      56. * 2 修改account表,恢复可用金额
      57. * @param ctx
      58. * @return
      59. */
      60. @Override
      61. @Transactional
      62. public boolean cancel(BusinessActionContext ctx) {
      63. //1. 获取事务Id
      64. String xid = ctx.getXid();
      65. //2. 查询冻结记录
      66. AccountFreeze freeze = accountFreezeMapper.selectById(xid);
      67. //判断是否空回滚
      68. if(freeze == null){
      69. freeze = new AccountFreeze();
      70. freeze.setFreezeMoney(0);
      71. freeze.setState(AccountFreeze.State.CANCEL);
      72. freeze.setXid(xid);
      73. freeze.setUserId(userId);
      74. accountFreeMapper.insert(freeze);
      75. return true;
      76. }
      77. //3. 回滚金额
      78. String userId = ctx.getActionContext("userId").toString();
      79. log.debug("开始回滚金额: {},userId={}", xid, userId);
      80. accountMapper.refund(userId, freeze.getFreezeMoney());
      81. //4. 更新冻结金额为0,且更新状态为cancel
      82. freeze = new AccountFreeze();
      83. freeze.setFreezeMoney(0);
      84. freeze.setState(AccountFreeze.State.CANCEL);
      85. freeze.setXid(xid);
      86. int count = accountFreezeMapper.updateById(freeze);
      87. log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
      88. //5. 本地事务控制
      89. return count==1;
      90. }
      91. }

      解决业务悬挂

      :::tips 在Try方法中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行Try业务 :::

      1. /**
      2. * @version 1.0
      3. * @description 说明
      4. * @package cn.itcast.account.service.impl
      5. */
      6. @Service
      7. @Slf4j
      8. public class AccountTCCServiceImpl implements AccountTCCService {
      9. @Autowired
      10. private AccountMapper accountMapper;
      11. @Autowired
      12. private AccountFreezeMapper accountFreezeMapper;
      13. /**
      14. * 用户账户扣款
      15. * 1 扣减account表可用金额
      16. * 2 记录冻结金额和事务状态到account_freeze表
      17. * @param userId
      18. * @param money
      19. *
      20. */
      21. @Override
      22. @Transactional
      23. public void deduct(String userId, int money) {
      24. String xid = RootContext.getXID();
      25. //判断Cancel是否执行过,避免业务悬挂
      26. AccountFreeze freeze = accountFreezeMapper.selectById(xid);
      27. if(freeze != null){
      28. //Cancel已经执行过,拒绝业务
      29. return;
      30. }
      31. //1. 扣减金额
      32. accountMapper.deduct(userId,money);
      33. log.debug("扣减金额成功: {},userId={}", xid, userId);
      34. //2. 添加冻结记录
      35. AccountFreeze freeze = new AccountFreeze();
      36. freeze.setFreezeMoney(money);
      37. freeze.setState(AccountFreeze.State.TRY);
      38. freeze.setXid(xid);
      39. freeze.setUserId(userId);
      40. accountFreezeMapper.insert(freeze);
      41. log.debug("冻结金额成功! {},userId={}", xid, userId);
      42. //3. 添加本地事务控制
      43. }
      44. /**
      45. * 确认业务,根据xid删除account_freeze表的冻结记录
      46. * @param ctx
      47. * @return
      48. */
      49. @Override
      50. public boolean confirm(BusinessActionContext ctx) {
      51. //1. 获取事务Id
      52. String xid = ctx.getXid();
      53. log.debug("进入confirm: {}", xid);
      54. //2. 通过事务Id删除数据
      55. int count = accountFreezeMapper.deleteById(xid);
      56. log.debug("删除冻结记录 {},count={}",xid,count);
      57. return 1==count;
      58. }
      59. /**
      60. * 撤消回滚,需要把冻结金额还原
      61. * 1 修改account_freeze表,冻结金额为0,state为2
      62. * 2 修改account表,恢复可用金额
      63. * @param ctx
      64. * @return
      65. */
      66. @Override
      67. @Transactional
      68. public boolean cancel(BusinessActionContext ctx) {
      69. //1. 获取事务Id
      70. String xid = ctx.getXid();
      71. //2. 查询冻结记录
      72. AccountFreeze freeze = accountFreezeMapper.selectById(xid);
      73. //判断是否空回滚
      74. if(freeze == null){
      75. freeze = new AccountFreeze();
      76. freeze.setFreezeMoney(0);
      77. freeze.setState(AccountFreeze.State.CANCEL);
      78. freeze.setXid(xid);
      79. freeze.setUserId(userId);
      80. accountFreeMapper.insert(freeze);
      81. return true;
      82. }
      83. //3. 回滚金额
      84. String userId = ctx.getActionContext("userId").toString();
      85. log.debug("开始回滚金额: {},userId={}", xid, userId);
      86. accountMapper.refund(userId, freeze.getFreezeMoney());
      87. //4. 更新冻结金额为0,且更新状态为cancel
      88. freeze = new AccountFreeze();
      89. freeze.setFreezeMoney(0);
      90. freeze.setState(AccountFreeze.State.CANCEL);
      91. freeze.setXid(xid);
      92. int count = accountFreezeMapper.updateById(freeze);
      93. log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
      94. //5. 本地事务控制
      95. return count==1;
      96. }
      97. }

      幂等处理

      :::tips 如果冻结记录,则先判断一下这条冻结记录的状态,如果状态为2(Cancel),说明已经执行过了,不能再重复执行业务 :::

      1. /**
      2. * @version 1.0
      3. * @description 说明
      4. * @package cn.itcast.account.service.impl
      5. */
      6. @Service
      7. @Slf4j
      8. public class AccountTCCServiceImpl implements AccountTCCService {
      9. @Autowired
      10. private AccountMapper accountMapper;
      11. @Autowired
      12. private AccountFreezeMapper accountFreezeMapper;
      13. /**
      14. * 用户账户扣款
      15. * 1 扣减account表可用金额
      16. * 2 记录冻结金额和事务状态到account_freeze表
      17. * @param userId
      18. * @param money
      19. *
      20. */
      21. @Override
      22. @Transactional
      23. public void deduct(String userId, int money) {
      24. String xid = RootContext.getXID();
      25. //判断Cancel是否执行过,避免业务悬挂
      26. AccountFreeze freeze = accountFreezeMapper.selectById(xid);
      27. if(freeze != null){
      28. //Cancel已经执行过,拒绝业务
      29. return;
      30. }
      31. //1. 扣减金额
      32. accountMapper.deduct(userId,money);
      33. log.debug("扣减金额成功: {},userId={}", xid, userId);
      34. //2. 添加冻结记录
      35. AccountFreeze freeze = new AccountFreeze();
      36. freeze.setFreezeMoney(money);
      37. freeze.setState(AccountFreeze.State.TRY);
      38. freeze.setXid(xid);
      39. freeze.setUserId(userId);
      40. accountFreezeMapper.insert(freeze);
      41. log.debug("冻结金额成功! {},userId={}", xid, userId);
      42. //3. 添加本地事务控制
      43. }
      44. /**
      45. * 确认业务,根据xid删除account_freeze表的冻结记录
      46. * @param ctx
      47. * @return
      48. */
      49. @Override
      50. public boolean confirm(BusinessActionContext ctx) {
      51. //1. 获取事务Id
      52. String xid = ctx.getXid();
      53. log.debug("进入confirm: {}", xid);
      54. //2. 通过事务Id删除数据
      55. int count = accountFreezeMapper.deleteById(xid);
      56. log.debug("删除冻结记录 {},count={}",xid,count);
      57. return 1==count;
      58. }
      59. /**
      60. * 撤消回滚,需要把冻结金额还原
      61. * 1 修改account_freeze表,冻结金额为0,state为2
      62. * 2 修改account表,恢复可用金额
      63. * @param ctx
      64. * @return
      65. */
      66. @Override
      67. @Transactional
      68. public boolean cancel(BusinessActionContext ctx) {
      69. //1. 获取事务Id
      70. String xid = ctx.getXid();
      71. //2. 查询冻结记录
      72. AccountFreeze freeze = accountFreezeMapper.selectById(xid);
      73. //判断是否空回滚
      74. if(freeze == null){
      75. freeze = new AccountFreeze();
      76. freeze.setFreezeMoney(0);
      77. freeze.setState(AccountFreeze.State.CANCEL);
      78. freeze.setXid(xid);
      79. freeze.setUserId(userId);
      80. accountFreeMapper.insert(freeze);
      81. return true;
      82. }
      83. //幂等处理
      84. if(AccountFreeze.State.CANCEL == freeze.getState().intValue()){
      85. //状态为Cancel,说明已经执行过Cancel,不能再重复处理
      86. return true;
      87. }
      88. //3. 回滚金额
      89. String userId = ctx.getActionContext("userId").toString();
      90. log.debug("开始回滚金额: {},userId={}", xid, userId);
      91. accountMapper.refund(userId, freeze.getFreezeMoney());
      92. //4. 更新冻结金额为0,且更新状态为cancel
      93. freeze = new AccountFreeze();
      94. freeze.setFreezeMoney(0);
      95. freeze.setState(AccountFreeze.State.CANCEL);
      96. freeze.setXid(xid);
      97. int count = accountFreezeMapper.updateById(freeze);
      98. log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
      99. //5. 本地事务控制
      100. return count==1;
      101. }
      102. }