一个connection对象可以创建一个或一个以上的statement对象_一个银行转账业务模型分析:大魏Java记57...

本文详细介绍了Java中的事务处理,包括事务的概念、JDBC操作数据库、JDBC批处理、事务处理方法,以及如何通过JDBC进行事务管理。接着探讨了集中式和分布式事务的区别,提到了2PC、补偿事务、TCC等分布式事务解决方案。文章还讨论了数据库连接池的重要性,讲解了DBCP和C3P0的使用方法。最后,通过DBUtils工具类展示了简化事务处理的示例。
摘要由CSDN通过智能技术生成

aefa92a50ffeebfa3111f9aad07bd1af.png

银行转账业务是一个典型的事务,我们先介绍基本概念,这样后面的代码看起来就比较容易理解了。

一、事务处理

“事务处理”这个名字的概念,相信大多数朋友都有所了解。我们看一下维基百科中关于“事务处理”的定义:

“在计算机科学中,事务是无法被分割的操作,事务处理就是被分割为个体的信息处理。事务必须作为一个完整的单元成功或失败,不可能存在部分完成的事务。”

看着是不是有点绕?

好,我们换个视角看“事务处理”,从数据库视角。

在数据库操作中,事务是由一条或者多条SQL语句组成的一个工作单元。只有当事中的所有操作都正常完成,整个事务才被提交到数据库,如果意向操作没有完成,则整个事务被撤销。

例如zhangsan给lisi转200元钱,对应的SQL是;

UPDATE ACCOUNT set MONEY=MONEY-200 WHERE NAME='zhangsan';

UPDATE ACCOUNT set MONEY=MONEY+200 WHERE NAME='lisi';

转钱这件事,就是一个事务处理。所以,事务是以数据为核心的,而数据是存在关系型数据库上的。因此,应用如何操作数据库就成为事务处理中的关键。

Java应用操作数据库的模块是JDBC。

二、JDBC

JBDC的全称为:Java Database Connectivity,是一套用于执行SQL语句的Java API。应用程序可以通过这套API连接到关系型数据库,并使用SQL语句来完成对数据库的查询、更新、删除工作。

0302bde23992f6c8985e15f9b681a991.png

我们先看一下JDBC API的内容。

在下面的三色框中,中间部分是JDBC常用的API。

第一部分是DataSource接口的实现;

第三名部分是DBUtils工具库;

5577c228b09eafd986f6586129b5cf10.png

JDBC的实现细节如下图所示,包含三大部分:

(1)JDBC驱动管理器:负责注册特定的JDBC驱动器,通过java.sql.DeviceManager类实现。

(2)Java驱动器API;其中最主要的接口是:java.sql.Driver接口。

(3)JDBC驱动器:一种数据库驱动,由数据库厂商创建。JDBC启动器实现了JDBC驱动器API,负责与特定的数据库链接,以及处理通信细节。

b3813ed7d5bf74120a7f0d62d1b96a3d.png

一个完整的JDBC分为六大步骤:

  1. 通过DriverManager加载并注册DB驱动

  2. 通过DeiverManager获取数据库连接(DB connection)

  3. 通过Connection对象获取Statement对象。

  4. 使用Statement执行SQL语句,生成结果集ResultSet。

  5. 操作结果集,取出结果。

  6. 释放数据库资源

需要注意的是:由于Driver类的源码中,已经在静态代码块中完成了数据库驱动的注册,所有为了避免数据库驱动被重复注册,主要在程序中加载驱动类即可。

下面我们查看一个完成的JDBC示例。下图中的标号,就对应上文的6个步骤。我们看到第六步释放了Statement和Connection资源。

92518986441613d23ce6d79149e2fff2.png

09f91d2948cb1be778269207fd212424.png

三、JDBC批处理

在实际开发中,经常需要向数据库发送多条SQL语句。这时如果逐条执行这些SQL语句,效率会很低。为此,JDBC提供了批处理机制,可以同时执行多条SQL语句。Statement和PreparedStatement都实现了批处理。

PreparedStatement是Statement的一个子类。PreparedStatement对象可以对SQL语句进行预编译。当相同的SQL语句再次执行时,数据库只需使用缓冲区的数据,而不需要对SQL语句再次编译,从而有效提高了数据的访问效率。

我们查看Statement加载和执行批处理的代码,如下图红框所示:

bf48e2e5ce6ab3f6f79995c2d138b05b.png

我们再看通过PreparedStatement加载和执行批处理的代码:

73c487c62b9b88db1328efdc898a1d80.png

很明显,PreparedStatement的方式更灵活。

需要注意的是:Statement和PreparedStatement的executeBactch()方法的范围值都是int[]类型的,所以能够进行批处理的SQL语句必须是INSERT、UPDATE、DELETE等返回值为int类型的SQK语句。

四、JDBC如何处理事务

针对JDBC处理事务的操作,在Connection接口中,提供了三个相关的方法,具体如下:

1. setAutoCommit(boolean autoCommit):设置是否自动提交事务。

2. commit(): 提交事务

3.rollback(): 撤销事务。

接下来,我们通过一个案例展示J如何通过JDBC进行事务处理。

首先看一个工具类:JDBCUtils

这个工具类避免我们每次操作都要书写:加载数据库驱动、检查数据连接以及关闭数据的连接的代码:

adda4d9a78bac7df02e54cf483596e1c.png

cb478b2c954b9e377847ae08f2d4ef81.png

接下来,新建一个类,用于在两个账号之间转账。

从下图可以看出,本类使用了工具类JDBCUtils中的方法访问数据库。

JDBC关闭事务自动提交、提交事务、回滚事务的位置,我用红框标出来了,比较好理解。

fa6a717a2638071f2ff50d82625141cf.png

62c3b88494963179f899d6aa7602af49.png

五、集中式事务与分布式事务的本质

事务(Transaction )的核心是操作数据。事务必须要满足ACID的标准。而集中式架构,是很容易保证事务的ACID的。ACID中比较难实现的是C,实时一致性。集中式事务实现的方式就是传统应用架构如JavaEE+关系型数据库。我们也管这种事务叫本地事务。这种事务可以实现强一致性。

37d935d0612e8051fb3386b34c9dc466.png

分布式架构,或者说分布式事务。分布式事务比较难以实现实时一致性,因此ACID不再适用。业内Bianc便提出了CAP和BASE等理论。所以说,集中式和分布式事务的根本区别是在于维护数据的强一致还是最终一致。

23d1f6514ddf7faa72cd76a50aecfffd.png

CAP 理论指出:无法设计一种分布式协议,使得同时完全具备CAP 三个属性,即1)该种协议 下的副本始终是强一致性,2)服务始终是可用的,3)协议可以容忍任何网络分区异常;分布式系统 协议只能在CAP 这三者间所有折中。

CAP 理论的定义很简单,CAP 三个字母分别代表了分布式系统中三个相互矛盾的属性:

  • Consistency (一致性):CAP 理论中的副本一致性特指强一致性;

  • Availiablity(可用性):指系统在出现异常时已经可以提供服务;

  • Tolerance to the partition of network (分区容忍):指系统可以对网络分区。这种异常情 况进行容错处理;

CAP 理论 的意义就在于明确提出了不要去妄图设计一种对CAP 三大属性都完全拥有的完美系统,因为这种系 统在理论上就已经被证明不存在。

BASE理论指的是:

  • Basically Available(基本可用)

  • Soft state(软状态)

  • Eventually consistent(最终一致性)

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

也就是说:在集中式事务中,我们可以实现完美的ACID。在分布式事务中,在得到分布式的收益下,我们也需要接受不完美。前文我们也提到过,事务处理中,比较难实现的是实时一致性,因此分布式系统的核心理念是:保证数据的最终一致性即可。

在CAP/Base理论的前提下,分布式事务的实现方式,主要分为2PC、补偿事务、TCC。2PC通常是事件驱动架构,补偿事务、TCC通常是通过代码实现,实现难度较高。因此,2PC应用更为广泛。

传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。

两阶段提交顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts)。

在阶段1中,coordinator发起一个提议,分别问询各participant是否接受。

54148619667c3d79e00b0374714172e9.png

在阶段2中,coordinator根据participant的反馈,提交或中止事务,如果participant全部同意则提交,只要有一个participant不同意就中止。

近年来兴起的微服务,也大量使用2PC,即事件驱动的分布式架构。事件驱动架构中,我们会使用到消息中间件如AMQ、Kafka等。

关于AMQ和Kafka容器化实现,可以参考:

https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-building-distributed-message-platform-based-on-openshift/index.html

关于分布式的技术细节,请参考:

分布式系统解析与实现

六、数据库连接池

 在前文中,我们提到了Java应用通过JDBC操作数据库,共分为六大步骤,其中重要的一个环节就是创建访问数据库连接(Connection)。

我们知道,每次创建和断开Connection对象都会消耗一定的时间和I/O资源。为了避免频繁地创建数据库连接,工程师么提出了数据库连接池技术。他负责分配、管理是释放数据库连接,他允许应用程序复用现有的数据库连接,而不是重新建立,如下图所示:

7340283536b83209c6eb3a09c2298c4a.png

数据库连接池在初始化时,会创建一定数量的数据库连接放到连接池中,当应用程序访问DB时,不是直接创建Connection,而是先从连接池申请Connection(如果池子中没有则新建Conection),使用完毕后,连接池会自动回收。

JDBC提供javax.sql.DataSource接口用于获取DB的Connection。获取Connection有两个方法:

  • Connection getConenction()

  • Connection getConnection(String username, String password)

这两个重载的方法都可以获取Connection对象,第一个是通过无参方式创建与DB的连接,第二种方法是通过传入登录信息等内容创建与数据库的连接。

我们通常把实现了javax.sql.DataSource接口的类称为数据源,而数据源中包含数据库连接池。我们用一张形象的图说明数据、数据库、数据源的关系:

a90d88153d3e27de8302dec30efb51bc.png

现在最常用的数据源有两种:

  • DBCP

  • C3P0

七、DBCP

DBCP全称是:DataBase Connection Pool。它是Apache下的开源项目,也是Tomcat使用的连接池组件。

单独使用DBCP的话,需要单独导入一下两个包:

1.commons-dbcp.jar包:DBCP数据源的实现包,包含操作数据库连接信息和数据库连接池初始化信息的方法,并实现了javax.sql.DataSource接口的getConnection()方法。

2.commons-pool.jar:为commons-dbcp.jar中的方法提供支持。

commons-dbcp.jar中包含两个核心的类:BasicDataSource、BasicSourceFactory。

BasicDataSource是BasicSource接口的实现类,包括设置数据源对象的方法:

2a9dfaaa88141471f8705d0421fe2736.png

BasicDataSource可以直接创建数据源对象。我们通过代码进行说明。

我们查看下图红框的标识:我们导入了BasicDataSource类、通过BasicDataSource()方法创建数据源实例bds、通过bds加载mysql的驱动、通过bds设置数据库URL/用户名/密码、将bds赋值ds、通过ds获取数据库连接。

17668e583a2718ff4561bb51a39f0c7a.png

BasicSourceFactory工厂类读取配置文件,然后创建数据对象,然后获取连接对象。

我们先查看配置文件:

06c3ee77874578987c14553c1c252851.png

我们查看代码,关注红框的标识:

我们导入Properities/DataSource/BasicDataSourceFactory类、创建配置文件对象prop、将配置文件内容加载到prop中、以prop为参数通过BasicSourceFactory创建数据源对象ds、通过ds创建数据库连接对象conn、获取数据库连接信息metaData、通过metaData打印相关信息。

ba1592a1ac5685870377badd6a753b2d.png

027cb501a90a0140abce6aa54d1c20c1.png

八、C3P0

C3P0是最流行的开源数据库连接池,实现了DataSource数据源接口,支持JDBC2和JDBC3的标准规范。开源框架Hibernate和Spring都是使用C3P0数据源。

C3P0的核心类是ComboPooledDataSource,它是DataSource接口类的实现。

C3P0的核心类ComboPooledDataSource的常用方法如下:

3023476e3717bdd0c05dc7a6bdcdbd69.png

ComboPooledDataSource有两个构造方法:

  • 无参构造方法:ComboPooledDataSource()

  • 有参构造方法:ComboPooledDataSource(String configName)

我们首先利用无参构造方法,展示通过ComboPooledDataSource类直接创建数据源对象:

734fd446b25f95bb0f9557a368cea609.png

代码输出结果如下所示:

e173f1b4d18a15e07a91e64e3911c19d.png

接下来,我们利用有参构造方法,读取c3p0-config.xml的配置文件(必须是这个名字,而且放到该项目的src目录下),创建数据源对象,然后获取数据库连接对象。

我们首先查看参数文件内容:

9d20c0faf97024a8cf4d66ca3fc57c76.png

我们查看代码:

559b6783a32fefd06d77c67a34caf18e.png

代码执行结果如下:

a1dcf48bc48590e9bd8f6e043612786c.png

九、DButils工具类

为了简单使用JDBC,Apache提供了一个工具类库commons-dbutils。它是操作数据库的一个组件,实现了对JDBC的封装,简化了代码工作量。

commons-dbutils包含两个核心类和一个接口:

两个核心类:

  • org.apache.commons.dbutils.DButils:主要用于关闭连接、装载JDBC驱动程序之类的常规工作提供方法。

  • org.apache.commons.dbutils.QueryRunner:主要用于执行增删改查操作。

一个接口:

  • org.apache.commons.dbutils.ResultSetHandler:主要用于处理ResultSet结果集。

十、DBuTils处理事务

我们如果使用连接池访问DB,就很难保证一个事务只用一个connection。但在不同的Connection中的数据无法共享。这就需要借助ThreadLocal类来实现在一个线程里记录变量。

我们可以生成一个Connection放在线程里,只要这个线程中的任何对象都可以共享这个链接,当线程结束后就删除这个Connection。

接下来我们模拟银行间的转账事务,来理解DBUtils的作用。

首先创建数据库表acoount作为账务记录表,插入数据:

a1939a73e0dd5f8eac5cf2e5e0defca5.png

fceaf32d38ce453186cb5619e2a7e02e.png

查看account数据库表,内容如下,我们看到a账户有1000、B账户有1000:

bd0b61865257d8c50b27ebf7fa4a0537.png

创建实体类Account,通过以下代码我们可以看出这个实体类包含对Account表操作的方法(get/set)

cfd91cbafcd793b5eeff98186ede4dd6.png

创建类JDBCUtils这个类一共有三大段代码,该类封装了创建Connection、开启事务关闭事务的方法:

c57a56d6f82c11c0cb69218000835cf6.png

本类代码第二段定义了开始开启事务和提交事务的方法。我们看到开启事务前必须要先获取Connection。我们看到提交事务获取Connection是从threadLocal获取的。

97cdb5ae90eb3d3818dc5768aed69295.png

第三段代码实现了回滚事务和关闭事务的方法。我们看到回滚和关闭事务前的Connection是从threadLocal获取的。16eedd9e250778cab5f97dcaa3effd64.png

创建AccountDao类,该类封装了转账所需的数据库操作,包括查询用户、转入、转出操作,代码实现如下,下面代码实现了查询和Update SQL,下面代码导入了dbutils类:

3acc1a8b49550110d374c3dc3acf531a.png

8c3a6ac969e4909dc8f193eca45aa3cf.png

创建Business类,这个类包含转账过程的逻辑方法,导入了封装事务操作的JDBCUtils类和封装数据库操作的AccountDao类,完成后转账操作,我们可以看到下面代码定义了一个名为Transfer的静态方法:9ee34938f6eb5134b40f344f41d30f1b.png

调用transfer方法,并注入参数,如下图所示:

589835557f48e66818f6f546af0bbdd7.png

执行结果如下,我们看到转账成功:

4c8d26539491e8fc1ce71beba633b562.png

参考文献:

《Java Web程序开发入门》-清华大学出版社

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值