在上一篇《java事务(二)——本地事务》中已经提到了事务的类型,并对本地事务做了说明。而分布式事务是跨越多个数据源来对数据来进行访问和更新,在JAVA中是使用JTA(Java Transaction API)来实现分布式的事务管理的。但是在本篇中并不会说明如何使用JTA,而是在不依赖其他框架以及jar包的情况下自己来实现分布式事务,作为对分布式事务的一个理解。

假设现在有两个数据库,可以是在一台机器上也可以是在不同机器上,现在要向其中一个数据库更新用户账户信息,另外一个数据库新增用户的消费信息。首先说明一下,分布式事务也是事务,在事务特性的那篇博客中就已经说明了事务的四个特性:原子性、一致性、隔离性和持久性,那么分布式事务也必然是符合这四个特性的,这就要求同时对两个数据库进行数据访问和更新的时候是作为一个单独的工作单元来进行处理,并且同时成功或者失败后进行回滚。但是在说明本地事务的时候已经提到了,本地事务是基于连接的,现在有两个数据库,分别保存数据,那么为了实现这个事务,必然会有两个数据库连接,这似乎是与事务基于连接的说法相悖。现在举个例子:之前回老家去了一趟医院,后来在办理出院手续的时候是这样的,办理出院时需要护士站的主任医生填写出院单,然后携带结账单到收费处缴纳费用并去药房取药,然后回护士站盖章,出院手续办理完毕。如果把不同地点的窗口看成是不同的连接,那么实现办理出院手续这个事务就必须保证在每个业务窗口上的事务都是成功的,最后出院手续才算真正完成。在最终盖章的时候,需要查看每个窗口给出的单子是否是已办理的,只有综合起来所有的单子才能判定出院手续是否成功。这主要就是为了说明分布式事务实现的关键其实是管理每个连接上的事务,用一个东西来判定每个连接上的事务执行情况,综合起来作为分布式事务执行成功与否的依据。这大概就是事务管理器要做的事情。虽然这个例子并不太恰当,很有挑毛病的地方,但是在不太钻牛角尖的情况下,还是可以用来说明要表达的东西的。

实现例子

我打开了两台虚拟机,分别命令为node1、node2,每台虚拟机上都安装了MySQL数据库,现在向node1上的数据库更新用户账户信息,向node2上的数据库新增用户消费信息。

 在node1上创建账户表,建表语句如下:

  1. CREATE TABLE ACCOUNTS
  2. (
  3. ID INT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  4. CUSTOMER_NO VARCHAR(25) NOT NULL COMMENT '客户号',
  5. CUSTOMER_NAME VARCHAR(25) NOT NULL COMMENT '客户名称',
  6. CARD_ID VARCHAR(18) NOT NULL COMMENT '身份证号',
  7. BANK_ID VARCHAR(25) NOT NULL COMMENT '开户行ID',
  8. BALANCE DECIMAL NOT NULL COMMENT '账户余额',
  9. CURRENCY VARCHAR(10) NOT NULL COMMENT '币种',
  10. PRIMARY KEY (ID)
  11. )
  12. COMMENT = '账户表' ;

然后向表中插入一条记录,如下图:

在node2上创建用户消费历史表,建表语句如下:

  1. CREATE TABLE USER_PURCHASE_HIS
  2. (
  3. ID INT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  4. CUSTOMER_NO VARCHAR(25) NOT NULL COMMENT '客户号',
  5. SERIAL_NO VARCHAR(32) NOT NULL COMMENT '交易流水号',
  6. AMOUNT DECIMAL NOT NULL COMMENT '交易金额',
  7. CURRENCY VARCHAR(10) NOT NULL COMMENT '币种',
  8. REMARK VARCHAR(100) NOT NULL COMMENT '备注',
  9. PRIMARY KEY (ID)
  10. )
  11. COMMENT = '用户消费历史表';

下面实现一个简陋的例子,代码如下:

1、创建DBUtil类,用来获取和关闭连接

  1. package person.lb.example1;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.DriverManager;
  5. import java.sql.ResultSet;
  6. import java.sql.SQLException;
  7. import java.sql.Statement;
  8.  
  9. public class DBUtil {
  10.  
  11. static {
  12. try {
  13. //加载驱动类
  14. Class.forName("com.mysql.jdbc.Driver");
  15. } catch (ClassNotFoundException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19.  
  20. //获取node1上的数据库连接
  21. public static Connection getNode1Connection() {
  22. Connection conn = null;
  23. try {
  24. conn = (Connection) DriverManager.getConnection(
  25. "jdbc:mysql://192.168.0.108:3306/TEST",
  26. "root",
  27. "root");
  28. } catch (SQLException e) {
  29. e.printStackTrace();
  30. }
  31. return conn;
  32. }
  33.  
  34. //获取node2上的数据库连接
  35. public static Connection getNode2Connection() {
  36. Connection conn = null;
  37. try {
  38. conn = (Connection) DriverManager.getConnection(
  39. "jdbc:mysql://192.168.0.109:3306/TEST",
  40. "root",
  41. "root");
  42. } catch (SQLException e) {
  43. e.printStackTrace();
  44. }
  45. return conn;
  46. }
  47.  
  48. //关闭连接
  49. public static void close(ResultSet rs, Statement st, Connection conn) {
  50. try {
  51. if(rs != null) {
  52. rs.close();
  53. }
  54. if(st != null) {
  55. st.close();
  56. }
  57. if(conn != null) {
  58. conn.close();
  59. }
  60. } catch (SQLException e) {
  61. // TODO Auto-generated catch block
  62. e.printStackTrace();
  63. }
  64. }
  65. }

2、创建XADemo类,用来测试事务

  1. package person.lb.example1;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.SQLException;
  5. import java.sql.Statement;
  6.  
  7. public class XADemo {
  8.  
  9. public static void main(String[] args) {
  10.  
  11. //获取连接
  12. Connection node1Conn = DBUtil.getNode1Connection();
  13. Connection node2Conn = DBUtil.getNode2Connection();
  14. try {
  15. //设置连接为非自动提交
  16. node1Conn.setAutoCommit(false);
  17. node2Conn.setAutoCommit(false);
  18. //更新账户信息
  19. updateAccountInfo(node1Conn);
  20. //增加用户消费信息
  21. addUserPurchaseInfo(node2Conn);
  22. //提交
  23. node1Conn.commit();
  24. node2Conn.commit();
  25. } catch (SQLException e) {
  26. e.printStackTrace();
  27. //回滚
  28. try {
  29. node1Conn.rollback();
  30. node2Conn.rollback();
  31. } catch (SQLException e1) {
  32. e1.printStackTrace();
  33. }
  34. } finally {
  35. //关闭连接
  36. DBUtil.close(null, null, node1Conn);
  37. DBUtil.close(null, null, node2Conn);
  38. }
  39. }
  40.  
  41. /**
  42. * 更新账户信息
  43. * @param conn
  44. * @throws SQLException
  45. */
  46. private static void updateAccountInfo(Connection conn) throws SQLException {
  47. Statement st = conn.createStatement();
  48. st.execute("UPDATE ACCOUNTS SET BALANCE = CAST('9900.00' AS DECIMAL) WHERE CUSTOMER_NO = '88888888' ");
  49. }
  50.  
  51. /**
  52. * 增加用户消费信息
  53. * @param conn
  54. * @throws SQLException
  55. */
  56. private static void addUserPurchaseInfo(Connection conn) throws SQLException {
  57. Statement st = conn.createStatement();
  58. st.execute("INSERT INTO USER_PURCHASE_HIS(CUSTOMER_NO, SERIAL_NO, AMOUNT, CURRENCY, REMARK) "
  59. + " VALUES ('88888888', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 100, 'CNY', '买衣服')");
  60. }
  61.  
  62. }

  这是一个没有发生任何异常的例子,执行结果是nod1上ACCOUNTS 表中的BALANCE字段的值成功更新为9900,而node2上USER_PURCHASE_HIS表中新增了一条记录,两个连接上的事务都成功完成,事务目标实现。如果反向测试一下,更改Insert语句,把其中某一个要插入的值改为NULL,由于字段都是非空限制,所以会发生异常,这个连接上的事务会失败,那么跟它关联的node1上的事务也必须回滚,不对数据库进行任何更改。经测试,结果与预期目标一致。说明这个例子是符合事务特性的。

  但是这个例子不管是从代码的可读性和可维护性上来说都是比较差的。在使用spring开发项目的时候,配置了事务管理器以后,在我们的业务逻辑中几乎是察觉不到事务控制的,而且也看不到事务控制的代码。那么究竟spring中是怎么实现的事务控制呢,这篇博客中不会详细说明,但是要提到两个东西,事务管理器和资源管理器,现在自己来实现一个简单的事务管理器和资源管理器来对事务进行控制。

代码示例如下:

1、创建AbstractDataSource 类

  1. package person.lb.datasource;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.SQLException;
  5.  
  6. public abstract class AbstractDataSource {
  7.  
  8. //获取连接
  9. public abstract Connection getConnection() throws SQLException ;
  10. //关闭连接
  11. public abstract void close() throws SQLException;
  12.  
  13. }

2、创建Node1DataSource 类,用来连接node1上的数据库

  1. package person.lb.datasource;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.DriverManager;
  5. import java.sql.SQLException;
  6.  
  7. public class Node1DataSource extends AbstractDataSource {
  8.  
  9. //使用ThreadLocal类保存当前线程使用的Connection
  10. protected static final ThreadLocal<Connection> threadSession = new ThreadLocal<Connection>();
  11.  
  12. static {
  13. try {
  14. //加载驱动类
  15. Class.forName("com.mysql.jdbc.Driver");
  16. } catch (ClassNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20.  
  21. private final static Node1DataSource node1DataSource = new Node1DataSource();
  22.  
  23. private Node1DataSource() {}
  24.  
  25. public static Node1DataSource getInstance() {
  26. return node1DataSource;
  27. }
  28.  
  29. /**
  30. * 获取连接
  31. */
  32. @Override
  33. public Connection getConnection() throws SQLException {
  34. Connection conn = null;
  35. if(threadSession.get() == null) {
  36. conn = (Connection) DriverManager.getConnection(
  37. "jdbc:mysql://192.168.0.108:3306/TEST",
  38. "root",
  39. "root");
  40. threadSession.set(conn);
  41. } else {
  42. conn = threadSession.get();
  43. }
  44. return conn;
  45. }
  46.  
  47. /**
  48. * 关闭并移除连接
  49. */
  50. @Override
  51. public void close() throws SQLException {
  52. Connection conn = threadSession.get();
  53. if(conn != null) {
  54. conn.close();
  55. threadSession.remove();
  56. }
  57. }
  58.  
  59. }

3、创建Node2DataSource类,用来连接node2机器上的数据库

  1. package person.lb.datasource;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.DriverManager;
  5. import java.sql.SQLException;
  6.  
  7. public class Node2DataSource extends AbstractDataSource {
  8.  
  9. //使用ThreadLocal类保存当前线程使用的Connection
  10. protected static final ThreadLocal<Connection> threadSession = new ThreadLocal<Connection>();
  11.  
  12. static {
  13. try {
  14. //加载驱动类
  15. Class.forName("com.mysql.jdbc.Driver");
  16. } catch (ClassNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20.  
  21. private static final Node2DataSource node2DataSource = new Node2DataSource();
  22.  
  23. private Node2DataSource() {};
  24.  
  25. public static Node2DataSource getInstance() {
  26. return node2DataSource;
  27. }
  28.  
  29. /**
  30. * 获取连接
  31. */
  32. @Override
  33. public Connection getConnection() throws SQLException {
  34. Connection conn = null;
  35. if(threadSession.get() == null) {
  36. conn = (Connection) DriverManager.getConnection(
  37. "jdbc:mysql://192.168.0.109:3306/TEST",
  38. "root",
  39. "root");
  40. threadSession.set(conn);
  41. } else {
  42. conn = threadSession.get();
  43. }
  44. return conn;
  45. }
  46.  
  47. /**
  48. * 关闭并移除连接
  49. */
  50. @Override
  51. public void close() throws SQLException {
  52. Connection conn = threadSession.get();
  53. if(conn != null) {
  54. conn.close();
  55. threadSession.remove();
  56. }
  57. }
  58. }

4、创建Node1Dao类,在node1的数据库中更新账户信息

  1. package person.lb.dao;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.SQLException;
  5. import java.sql.Statement;
  6.  
  7. import person.lb.datasource.Node1DataSource;
  8.  
  9. public class Node1Dao {
  10.  
  11. private Node1DataSource dataSource = Node1DataSource.getInstance();
  12.  
  13. /**
  14. * 更新账户信息
  15. * @throws SQLException
  16. */
  17. public void updateAccountInfo() throws SQLException {
  18. Connection conn = dataSource.getConnection();
  19. Statement st = conn.createStatement();
  20. st.execute("UPDATE ACCOUNTS SET BALANCE = CAST('9900.00' AS DECIMAL) WHERE CUSTOMER_NO = '88888888' ");
  21. }
  22. }

5、创建Node2Dao,在node2机器上增加用户消费信息

  1. package person.lb.dao;
  2.  
  3. import java.sql.Connection;
  4. import java.sql.SQLException;
  5. import java.sql.Statement;
  6.  
  7. import person.lb.datasource.Node2DataSource;
  8.  
  9. public class Node2Dao {
  10.  
  11. private Node2DataSource dataSource = Node2DataSource.getInstance();
  12.  
  13. /**
  14. * 增加用户消费信息
  15. * @throws SQLException
  16. */
  17. public void addUserPurchaseInfo() throws SQLException {
  18. Connection conn = dataSource.getConnection();
  19. Statement st = conn.createStatement();
  20. st.execute("INSERT INTO USER_PURCHASE_HIS(CUSTOMER_NO, SERIAL_NO, AMOUNT, CURRENCY, REMARK) "
  21. + " VALUES ('88888888', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', null, 'CNY', '买衣服')");
  22. }
  23. }

6、创建NodeService类,把两个操作作为一个事务来执行

  1. package person.lb.service;
  2.  
  3. import java.sql.SQLException;
  4.  
  5. import person.lb.dao.Node1Dao;
  6. import person.lb.dao.Node2Dao;
  7. import person.lb.transaction.TransactionManager;
  8.  
  9. public class NodeService {
  10.  
  11. public void execute() {
  12. //启动事务
  13. TransactionManager.begin();
  14.  
  15. Node1Dao node1Dao = new Node1Dao();
  16. Node2Dao node2Dao = new Node2Dao();
  17. try {
  18. node1Dao.updateAccountInfo();
  19. node2Dao.addUserPurchaseInfo();
  20. //提交事务
  21. TransactionManager.commit();
  22. } catch (SQLException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }

7、最后是测试类TestTx

  1. package person.lb.test;
  2.  
  3. import person.lb.service.NodeService;
  4.  
  5. public class TestTx {
  6.  
  7. public static void main(String[] args) {
  8. NodeService nodeService = new NodeService();
  9. nodeService.execute();
  10. }
  11. }

经测试,与第一个例子效果一致,但是从代码上来说要比第一个例子的可读性和可维护性高。不过这个例子并不能说明分布式事务中的事务管理器和资源管理器的真正原理,也不是一个可使用的代码,毕竟存在缺陷,而且dao层需要抛出异常才能实现事务的回滚。我想,作为一个理解分布式事务的作用的例子是够了。

最后是这篇博客中的源码:TransactionDemo.rar

java事务(三)——自己实现分布式事务的更多相关文章

  1. 分布式事务(4)---RocketMQ实现分布式事务项目

    RocketMQ实现分布式事务 有关RocketMQ实现分布式事务前面写了一篇博客 1.RocketMQ实现分布式事务原理 下面就这个项目做个整体简单介绍,并在文字最下方附上项目Github地址. 一 ...

  2. 分布式事务(3)---RocketMQ实现分布式事务原理

    分布式事务(3)-RocketMQ实现分布式事务原理 之前讲过有关分布式事务2PC.3PC.TCC的理论知识,博客地址: 1.分布式事务(1)---2PC和3PC原理 2.分布式事务(2)---TCC ...

  3. MySQL的本地事务、全局事务、分布式事务

    本地事务 事务特性:ACID,其中C一致性是目的,AID是手段. 实现隔离性 写锁:数据加了写锁,其他事务不能写也不能读. 读锁:数据加了读锁,其他事务不能加写锁可以加读锁,可以允许自己升级为写锁. ...

  4. 分布式事务(三)mysql对XA协议的支持

    系列目录 分布式事务(一)原理概览 分布式事务(二)JTA规范 分布式事务(三)mysql对XA协议的支持 分布式事务(四)简单样例 分布式事务(五)源码详解 分布式事务(六)总结提高 引子 从Mys ...

  5. java基础之----分布式事务tcc

    最近研究了一下分布式事务框架,ttc,总体感觉还可以,当然前提条件下是你要会使用这个框架.下面分层次讲,尽量让想学习的同学读了这篇文章能加以操作运用.我不想废话,直接上干货. 一.什么是tcc?干什么 ...

  6. 分布式事务-Sharding 数据库分库分表

      Sharding (转)大型互联网站解决海量数据的常见策略 - - ITeye技术网站 阿里巴巴Cobar架构设计与实践 - 机械机电 - 道客巴巴 阿里分布式数据库服务原理与实践:沈询_文档下载 ...

  7. spring boot + druid + mybatis + atomikos 多数据源配置 并支持分布式事务

    文章目录 一.综述 1.1 项目说明 1.2 项目结构 二.配置多数据源并支持分布式事务 2.1 导入基本依赖 2.2 在yml中配置多数据源信息 2.3 进行多数据源的配置 三.整合结果测试 3.1 ...

  8. 【分布式事务】使用atomikos+jta解决分布式事务问题

    一.前言 分布式事务,这个问题困惑了小编很久,在3个月之前,就间断性的研究分布式事务.从MQ方面,数据库事务方面,jta方面.近期终于成功了,使用JTA解决了分布式事务问题.先写一下心得,后面的二级提 ...

  9. LCN分布式事务管理(一)

    前言 好久没写东西了,9月份换了份工作,一上来就忙的要死.根本没时间学东西,好在新公司的新项目里面遇到了之前没遇到过的难题.那遇到难题就要想办法解决咯,一个请求,调用两个服务,同时操作更新两个数据库. ...

随机推荐

  1. java 自制Tomcat Andorid IOS 端 证书

    java 自制证书 最近做项目用到Https 需要自制各种证书,Tomcat 用的JKS 格式, Andorid 端使用 BKS 格式, IOS 端使用 P12格式正式, 以及各种证书格式之间的转换. ...

  2. 微信小程序学习笔记(7)--------布局基础

    ui布局基础 一.flex布局 1.flex的容器和元素 2.flex容器属性详解     1>flex-direction不仅设置元素的排列方向,还设置主轴和交叉轴如下图主轴是由上到下 2&g ...

  3. 20145217《网络对抗》 逆向及BOF进阶实践学习总结

    20145217<网络对抗> 逆向及BOF进阶实践学习总结 实践目的 1.注入shellcode 2.实现Return-to-libc攻击 知识点学习总结 Shellcode实际是一段代码 ...

  4. hadoop中mapreduce的默认设置

    MR任务默认配置: job.setMapperClass() Mapper Mapper将输入的<key,value>对原封不动地作为中间结果输出 job.setMapperOutputK ...

  5. Contest-hunter 暑假送温暖 SRM08

    01-07都没写...然后突然来写貌似有点突兀啊...不管了,难得前排记录一下... 吐槽一下赛制...不得不说很强... cf 套oi...很创新...不过还是兹磁ACM或者CF A-1 数据才2& ...

  6. jQuery.fn.extend() jQuery.extend()

    是jQuery为开发插件提拱了两个方法 jQuery.fn jQuery.fn = jQuery.prototype = { init: function( selector, context ) { ...

  7. DataStage系列教程 (Pivot_Enterprise 行列转换)

    有人提到Pivot_Enterprise这个组件,之前没有用过,今天捣腾了会,写下来供以后参考,如果有什么不对的,还请多指出,谢谢! Pivot_Enterprise主要用来进行行列转换. 1 示例 ...

  8. LeetCode第[17]题(Java):Letter Combinations of a Phone Number

    题目:最长公共前缀 难度:EASY 题目内容: Given a string containing digits from 2-9 inclusive, return all possible let ...

  9. charles抓包工具的使用:概述

    一. 什么是包 用户和后台客户端之间的请求数据,都是以包的形式来传递的,具体要深究,可以去看看这方面的网络知识 二. 为何要抓包 1) 可以用来分析网络流量 2) 可以用来破译抓来的数据,比如密码之类 ...

  10. mysql数据库(三):查询的其他用法

    一. 查询—IN的用法 语法:select ... from 表名 where 字段 a in (值b, 值c, 值d...) 等价于 select ... from 表名 where 字段a=值b ...