java事务(三)——自己实现分布式事务

时间:2022-09-10 14:01:28

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

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

实现例子

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

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

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

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

java事务(三)——自己实现分布式事务

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

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

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

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

package person.lb.example1;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement; public class DBUtil { static {
try {
//加载驱动类
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} //获取node1上的数据库连接
public static Connection getNode1Connection() {
Connection conn = null;
try {
conn = (Connection) DriverManager.getConnection(
"jdbc:mysql://192.168.0.108:3306/TEST",
"root",
"root");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
} //获取node2上的数据库连接
public static Connection getNode2Connection() {
Connection conn = null;
try {
conn = (Connection) DriverManager.getConnection(
"jdbc:mysql://192.168.0.109:3306/TEST",
"root",
"root");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
} //关闭连接
public static void close(ResultSet rs, Statement st, Connection conn) {
try {
if(rs != null) {
rs.close();
}
if(st != null) {
st.close();
}
if(conn != null) {
conn.close();
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

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

package person.lb.example1;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement; public class XADemo { public static void main(String[] args) { //获取连接
Connection node1Conn = DBUtil.getNode1Connection();
Connection node2Conn = DBUtil.getNode2Connection();
try {
//设置连接为非自动提交
node1Conn.setAutoCommit(false);
node2Conn.setAutoCommit(false);
//更新账户信息
updateAccountInfo(node1Conn);
//增加用户消费信息
addUserPurchaseInfo(node2Conn);
//提交
node1Conn.commit();
node2Conn.commit();
} catch (SQLException e) {
e.printStackTrace();
//回滚
try {
node1Conn.rollback();
node2Conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
//关闭连接
DBUtil.close(null, null, node1Conn);
DBUtil.close(null, null, node2Conn);
}
} /**
* 更新账户信息
* @param conn
* @throws SQLException
*/
private static void updateAccountInfo(Connection conn) throws SQLException {
Statement st = conn.createStatement();
st.execute("UPDATE ACCOUNTS SET BALANCE = CAST('9900.00' AS DECIMAL) WHERE CUSTOMER_NO = '88888888' ");
} /**
* 增加用户消费信息
* @param conn
* @throws SQLException
*/
private static void addUserPurchaseInfo(Connection conn) throws SQLException {
Statement st = conn.createStatement();
st.execute("INSERT INTO USER_PURCHASE_HIS(CUSTOMER_NO, SERIAL_NO, AMOUNT, CURRENCY, REMARK) "
+ " VALUES ('88888888', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 100, 'CNY', '买衣服')");
} }

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

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

代码示例如下:

1、创建AbstractDataSource 类

package person.lb.datasource;

import java.sql.Connection;
import java.sql.SQLException; public abstract class AbstractDataSource { //获取连接
public abstract Connection getConnection() throws SQLException ;
//关闭连接
public abstract void close() throws SQLException; }

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

package person.lb.datasource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException; public class Node1DataSource extends AbstractDataSource { //使用ThreadLocal类保存当前线程使用的Connection
protected static final ThreadLocal<Connection> threadSession = new ThreadLocal<Connection>(); static {
try {
//加载驱动类
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} private final static Node1DataSource node1DataSource = new Node1DataSource(); private Node1DataSource() {} public static Node1DataSource getInstance() {
return node1DataSource;
} /**
* 获取连接
*/
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
if(threadSession.get() == null) {
conn = (Connection) DriverManager.getConnection(
"jdbc:mysql://192.168.0.108:3306/TEST",
"root",
"root");
threadSession.set(conn);
} else {
conn = threadSession.get();
}
return conn;
} /**
* 关闭并移除连接
*/
@Override
public void close() throws SQLException {
Connection conn = threadSession.get();
if(conn != null) {
conn.close();
threadSession.remove();
}
} }

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

package person.lb.datasource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException; public class Node2DataSource extends AbstractDataSource { //使用ThreadLocal类保存当前线程使用的Connection
protected static final ThreadLocal<Connection> threadSession = new ThreadLocal<Connection>(); static {
try {
//加载驱动类
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} private static final Node2DataSource node2DataSource = new Node2DataSource(); private Node2DataSource() {}; public static Node2DataSource getInstance() {
return node2DataSource;
} /**
* 获取连接
*/
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
if(threadSession.get() == null) {
conn = (Connection) DriverManager.getConnection(
"jdbc:mysql://192.168.0.109:3306/TEST",
"root",
"root");
threadSession.set(conn);
} else {
conn = threadSession.get();
}
return conn;
} /**
* 关闭并移除连接
*/
@Override
public void close() throws SQLException {
Connection conn = threadSession.get();
if(conn != null) {
conn.close();
threadSession.remove();
}
}
}

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

package person.lb.dao;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement; import person.lb.datasource.Node1DataSource; public class Node1Dao { private Node1DataSource dataSource = Node1DataSource.getInstance(); /**
* 更新账户信息
* @throws SQLException
*/
public void updateAccountInfo() throws SQLException {
Connection conn = dataSource.getConnection();
Statement st = conn.createStatement();
st.execute("UPDATE ACCOUNTS SET BALANCE = CAST('9900.00' AS DECIMAL) WHERE CUSTOMER_NO = '88888888' ");
}
}

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

package person.lb.dao;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement; import person.lb.datasource.Node2DataSource; public class Node2Dao { private Node2DataSource dataSource = Node2DataSource.getInstance(); /**
* 增加用户消费信息
* @throws SQLException
*/
public void addUserPurchaseInfo() throws SQLException {
Connection conn = dataSource.getConnection();
Statement st = conn.createStatement();
st.execute("INSERT INTO USER_PURCHASE_HIS(CUSTOMER_NO, SERIAL_NO, AMOUNT, CURRENCY, REMARK) "
+ " VALUES ('88888888', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', null, 'CNY', '买衣服')");
}
}

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

package person.lb.service;

import java.sql.SQLException;

import person.lb.dao.Node1Dao;
import person.lb.dao.Node2Dao;
import person.lb.transaction.TransactionManager; public class NodeService { public void execute() {
//启动事务
TransactionManager.begin(); Node1Dao node1Dao = new Node1Dao();
Node2Dao node2Dao = new Node2Dao();
try {
node1Dao.updateAccountInfo();
node2Dao.addUserPurchaseInfo();
//提交事务
TransactionManager.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

7、最后是测试类TestTx

package person.lb.test;

import person.lb.service.NodeService;

public class TestTx {

    public static void main(String[] args) {
NodeService nodeService = new NodeService();
nodeService.execute();
}
}

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

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

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

  1. 分布式事务&lpar;4&rpar;---RocketMQ实现分布式事务项目

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

  2. 分布式事务&lpar;3&rpar;---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 &plus; druid &plus; mybatis &plus; atomikos 多数据源配置 并支持分布式事务

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

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

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

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

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

随机推荐

  1. js中转移符

    "<a href='javascript:;' onclick='javascript:changeChannelRuleStatus(\"" + options. ...

  2. Android与Dalvik

    自学android的同事说:Android 这个妈蛋!! 当初就应该选择c++/c 来开发.现在为了效率又搞ART.简直是折腾,art在android5.0 的时候就是默认了.前段时间还在学习andr ...

  3. Microsoft Community

    一.简介 Microsoft Community 是一个免费社区和讨论论坛,项目开发遇到的问题可以在这里进行提出和解答. 二.地址 http://answers.microsoft.com/zh-ha ...

  4. view视图文件中的input等输入框必须含有name属性,不然控制器里的动作formCollection是没有值的

    view视图文件中的input等输入框必须含有name属性,不然控制器里的动作formCollection是没有值的,就是没有name属性,后台获取不到值

  5. 重启Apache报错apache2&colon; Could not reliably determine the server&&num;39&semi;s fully qualified domain name&comma; using 127&period;0&period;1&period;1 for ServerName &period;&period;&period; waiting的解决方法

    启动apache提示 : apache2: Could not reliably determine the server's fully qualified domain name, using 1 ...

  6. python基础学习笔记第二天 内建方法(s t r)

    python的字符串内建函数 str.casefold()将字符串转换成小写,Unicode编码中凡是有对应的小写形式的,都会转换str.center()返回一个原字符串居中,并使用空格填充至长度 w ...

  7. 【转】Linux网络编程入门

    (一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端         网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...

  8. Struts2WebUtil

    一个简单的实用工具类 package cn.jorcen.commons.util; import javax.servlet.http.HttpServletRequest; import org. ...

  9. java基础(二十)IO流(三)

    这里有我之前上课总结的一些知识点以及代码大部分是老师讲的笔记 个人认为是非常好的,,也是比较经典的内容,真诚的希望这些对于那些想学习的人有所帮助! 由于代码是分模块的上传非常的不便.也比较多,讲的也是 ...

  10. SuperMap iObject入门开发系列之六管线区域查询

    本文是一位好友“托马斯”授权给我来发表的,介绍都是他的研究成果,在此,非常感谢. 管线区域查询功能针对单一管线图层进行区域多边形框选查询,然后将查询结果输出为列表,并添加定位和闪烁功能,效果如下图所示 ...