一. 什么是事务:
所谓事务,就是针对数据库的一组操作(多条sql)
位于同一个事务的操作具备同步的特点,也就是要么都成功,要么都失败
二. 事务的作用:
在实际中,我们的很多操作都是需要由多条sql来共同完成的,例如,A账户给B账户转账就会对应两条sql
update account set money = money - 100 where name = ‘a’;
update account set money = money + 100 where name = ‘b’;
假设第一条sql成功了,而第二条sql失败了,这样就会导致a账户损失了100元,而b账户并未得到100元
如果将两条sql放在一个sql中,当第二条语句失败时,第一条sql语句也同样不会生效,
这样a账户就不会有任何的损失
三. 事务的实现原理:
默认情况下,我们向数据库发送的sql语句是会被自动提交的,开启事务就是相当于关闭自动提交功能,改为手动提交,我们只需要将提交事务的操作放在最后一个操作,这样一来,如果在提交事务之前出现异常,由于没有执行提交操作,事务中未提交的操作就会被回滚掉
四. 在程序中使用 jdbc 开启事务
在使用 jdbc 操作数据库时,需要使用 Connection 对象对事务进行管理
// 开启事务
Connection.setAutoCommit(false); //设置自动提交为false
// 回滚事务
Connection.rollback();
//提交事务
Connection.commit();
在 jdbc 程序中我们还可以设置回滚点,让事务回滚到指定的回滚点,而不是自动回滚所有未提交的操作
需要将程序中的异常捕获,在catch语句块中回滚事务,在finally中提交事务
注意 ,将 Commit 操作放在 finally 中是为了保证提交未回滚的事务操作
五. 事务的特性
事务有四大特性,一般来讲,判断一个数据库是否支持事务,就看数据库是否支持这四个特性
1. 原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
2. 一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
3. 隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
4. 持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
六. 事务的隔离级别
1)多线程开启事务
由于数据库是多线程并发访问的,所以很容易出现多个线程同时开启事务的情况
多线程开事务容易引起 赃读、不可重复读、幻读 的情况发生
## 赃读:dirty read ##
是指一个事务读取了另一个事务未提交的数据,这是相当危险的。
设想一下,A要给B转账100元购买商品, 如果A开启了一个事务做了转账的工作
update account set money = money + 100 while name = ‘b’;
update account set money = money - 100 while name = ‘a’;
A先不提交事务,通知B来查询
这时B来查询账户,由于会读到A开启的事务中未提交的数据,就会发现A确实给自己转了100元,
自然就会给A发货,A等B发货后就将事务回滚,不提交,此时,B就会受到损失
## 不可重复读:non-repeatable read ##
是指事务中两次查询的结果不一致,原因是在查询的过程中其他事务做了更新的操作 update
例如,银行做报表,第一次查询A账户有100元,第二次查询A账户有200元,原因是期间A存了100元
这样就会导致一行多次统计的报表不一致
和脏读的区别是:
脏读是读取前一事务未提交的脏数据,不可重复读是在事务内重复读取了别的线程已提交的数据。
有的时候大家会认为这样的结果是正确的,没问题
我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
## 幻读:phantom read 又名虚读 ##
是指在一个事务内两次查询中数据笔数不一致
幻读和不可重复读有些类似,是指两次查询过程中,其他事务做了插入记录的操作,导致记录数有所增加insert
例如银行做报表统计account表中所有用户的总额时,此时总共五个账户,总金额为500元,这时有一个新的账户产生了,并且
存了100元,这时银行再统计会发现帐户总金额为600元了,造成虚读同样会使银行遇到同样的困惑
2)设置事务的隔离级别
为了避免多线程开事务引发的问题,我们需要将事务进行隔离
事务有四种隔离级别,不同的隔离级别可以防止不同的错误发生
serializable:可串行化,能避免脏读、不可重复读、幻读情况的发生
repeatable read:可重读,能避免脏读、不可重复读情况的发生
read committed:读取提交的内容,可避免脏读情况发生
read uncommitted:读取未提交的内容最低级别,避免不了任何情况
不同的需求就会采取不同的隔离级别, 而且不同的隔离级别他的性能也不一样
操作:
设置事务隔离级别
set transaction isolation level
查询当前事务隔离级别
select @@tx_isolation
查询看到的都是快照<其实就是设置事务前的一个拷贝,>
位于事务中的多次查询,如果隔离级别设置为repeatable read,那么多次查询读的就是一个快照, 说白了就是不更新快照
一. 为什么要传递Connection?
在前面的概述中我们知道, JDBC事务处理的作用对象为Connection, 因此要想控制操作在同一个事务里面,
我们必须要传递Connection, 确保使用的是同一个Connection.
二. 如何传递Connection?
本实例使用转账的例子: 即从A账户转100元到B账户, 这需要做两次update表操作
1. 代码结构图:
2. 建表语句:
- DROP TABLE IF EXISTS `account`;
- CREATE TABLE `account` (
- `id` int(10) NOT NULL,
- `name` varchar(20) NOT NULL,
- `money` int(20) NOT NULL,
- PRIMARY KEY (`id`)
- )
-
- INSERT INTO `account` VALUES ('1', 'lucy', '1000');
- INSERT INTO `account` VALUES ('2', 'lili', '1000');
3. 实体类:
- public class Account {
- private int id;
- private String name;
- private int money;
-
- // getter and setter
- }
4. C3P0连接池配置:
- <?xml version="1.0" encoding="UTF-8"?>
- <c3p0-config>
- <default-config>
- <property name="driverClass">com.mysql.jdbc.Driver</property>
- <property name="jdbcUrl">jdbc:mysql:///test</property>
- <property name="user">root</property>
- <property name="password">root</property>
-
- <property name="acquireIncrement">50</property>
- <property name="initialPoolSize">100</property>
- <property name="minPoolSize">50</property>
- <property name="maxPoolSize">1000</property>
- </default-config>
- </c3p0-config>
5. JDBC工具类:
- public class JDBCUtils {
- private static DataSource dataSource;
- static {
- // 加载C3P0连接池
- dataSource = new ComboPooledDataSource();
- }
-
- public static DataSource getDataSource() {
- return dataSource;
- }
-
- public static Connection getConnection() throws SQLException {
- return dataSource.getConnection();
- }
-
- }
6. 业务逻辑类:
- /**
- * 业务逻辑层
- */
- public class AccountService {
-
- public void transfer(Account outAccount, Account inAccount, int money) throws SQLException {
-
- // 开启 事务
- Connection conn = JDBCUtils.getConnection();
- conn.setAutoCommit(false);
-
- // 查询两个账户
- AccountDAO accountDAO = new AccountDAO();
- outAccount = accountDAO.findAccountById(outAccount.getId());
- inAccount = accountDAO.findAccountById(inAccount.getId());
-
- // 转账 - 修改原账户金额
- outAccount.setMoney(outAccount.getMoney() - money);
- inAccount.setMoney(inAccount.getMoney() + money);
-
- try {
- // 更新账户金额, 注意: 这里往Dao层传递连接
- accountDAO.update(outAccount, conn);
- // int x = 1 / 0;
- accountDAO.update(inAccount, conn);
-
- // 转账成功, 提交事务
- conn.commit();
- } catch (Exception e) {
- // 转账失败, 回滚事务
- conn.rollback();
- e.printStackTrace();
- }
- }
- }
7. Dao类:
- /**
- * DAO层: CRUD
- */
- public class AccountDAO {
-
- // 查询账户
- public Account findAccountById(int id) throws SQLException {
- String sql = "select * from account where id = ?";
- Object[] params = {id};
- QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDataSource());
- return queryRunner.query(sql, new BeanHandler<Account>(Account.class), params);
- }
-
- // 更新账户: 接受传递的连接
- public void update(Account account, Connection conn) throws SQLException {
- String sql = "update account set name = ?, money = ? where id = ?";
- Object[] params = {account.getName(), account.getMoney(), account.getId()};
- QueryRunner queryRunner = new QueryRunner();
- queryRunner.update(conn, sql, params);
- }
- }
8. 测试类:
- public class TransferTest {
-
- @Test
- public void transferTest() throws SQLException {
- Account out = new Account();
- out.setId(1);
-
- Account in = new Account();
- in.setId(2);
- AccountService accountService = new AccountService();
- accountService.transfer(out, in, 100);
- }
- }
三. 总结:
上面传递Connection对象的方法虽然可以完成事务处理的目的, 但是这样的做法是丑陋的, 原因在于: 为了完成事务处理的目的,
我们需要将一个底层Connection类在service层和Dao层之间进行传递, Dao层的方法需要接受这个Connection对象, 这种做法是典型的API污染.
源码下载: http://download.csdn.net/detail/zdp072/7908249
一. 为什么使用ThreadLocal:
在上一篇博文中, 我们通过传递Connection的方式来控制事务, 这种方法可以达到目的, 但让人看的不爽,
如果涉及到调用多个service, 那我是不是还得从controller层传递Connection?
ThreadLocal的用法见上一篇博客, 该类保证一个类的实例变量在各个线程中都有一份单独的拷贝,
从而不会影响其他线程中的实例变量,所以ThreadLocal可以实现线程范围内数据共享。
二. 如何使用ThreadLocal:
1. 写一个TransactionManager类:
[java] view plain copy
- /**
- * 管理事务
- */
- public class TransactionManager {
- private static ThreadLocal<Connection> local = new ThreadLocal<Connection>();
-
- // 开启事务
- public static void beginTransaction() throws SQLException {
- Connection conn = JDBCUtils.getConnection();
- conn.setAutoCommit(false);
- // 将连接存入threadLocal
- local.set(conn);
- }
-
- // 回滚事务
- public static void rollback() throws SQLException {
- Connection conn = local.get();
- if (conn != null) {
- conn.rollback();
- conn.close();
- // 清空threadLocal
- local.remove();
- }
- }
-
- // 提交事务
- public static void commit() throws SQLException {
- Connection conn = local.get();
- if (conn != null) {
- conn.commit();
- // 清空threadLocal
- local.remove();
- }
- }
-
- // 关闭连接
- public static void close() throws SQLException {
- Connection conn = local.get();
- if (conn != null) {
- conn.close();
- // 清空threadLocal
- local.remove();
- }
- }
-
- // 获取数据库连接
- public static Connection getConnection() {
- return local.get();
- }
- }
使用ThreadLocal, 确保相同的线程获取到的是同一个连接.
2.修改业务处理类
[java] view plain copy
- /**
- * 业务逻辑层
- */
- public class AccountService {
-
- public void transfer(Account outAccount, Account inAccount, int money) throws SQLException {
- // 开启 事务
- TransactionManager.beginTransaction();
-
- // 查询两个账户
- AccountDAO accountDAO = new AccountDAO();
- outAccount = accountDAO.findAccountById(outAccount.getId());
- inAccount = accountDAO.findAccountById(inAccount.getId());
-
- // 转账 - 修改原账户金额
- outAccount.setMoney(outAccount.getMoney() - money);
- inAccount.setMoney(inAccount.getMoney() + money);
-
- try {
- // 更新账户金额
- accountDAO.update(outAccount);
- accountDAO.update(inAccount);
-
- // 转账成功, 提交事务
- TransactionManager.commit();
- } catch (Exception e) {
- // 转账失败, 回滚事务
- TransactionManager.rollback();
- e.printStackTrace();
- } finally {
- // 关闭连接
- TransactionManager.close();
- }
- }
- }
使用TransactionManager来管理事务, 代码变得更加简洁.
3. 修改Dao类
[java] view plain copy
- /**
- * DAO层: CRUD
- */
- public class AccountDAO {
- // 查询账户
- public Account findAccountById(int id) throws SQLException {
- String sql = "select * from account where id = ?";
- Object[] params = {id};
- QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDataSource());
- return queryRunner.query(sql, new BeanHandler<Account>(Account.class), params);
- }
-
- // 更新账户
- public void update(Account account) throws SQLException {
- String sql = "update account set name = ?, money = ? where id = ?";
- Object[] params = {account.getName(), account.getMoney(), account.getId()};
-
- // 从threadLocal中获取连接, 同一个线程拿到的是同一个连接
- Connection conn = TransactionManager.getConnection();
- QueryRunner queryRunner = new QueryRunner();
- queryRunner.update(conn, sql, params);
- }
- }
不需要传递Connection, 直接从TransactionManager中获取连接.
三. 总结:
service和dao都是通过TransactionManager来获取Connection, 同一个线程中, 它们在整个事务处理过程中使用了相同的Connection对象, 所以事务会处理成功, dao中没有接受和业务无关的对象, 消除了api污染, 另外使用TransactionManager来管理事务, 使service层的代码变简洁了.
源码下载: http://download.csdn.net/detail/zdp072/7908261
一. 前言:
在上一篇博文中, 我们使用TransactionManager和ThreadLocal完成线程安全的事务管理,不知大家有没有发现,AccountService代码充斥着很多事务处理的代码,其实这些代码在很多方法里面都是重复出现,我们可以使用Template模式进行优化。
二. 实现:
1. 写一个模板类:TransactionTemplate
[java] view plain copy
- /**
- * 模板类
- */
- public abstract class TransactionTemplate {
-
- public void doJobInTransaction() throws SQLException {
- try {
- // 开启事务
- TransactionManager.beginTransaction();
-
- // 执行业务方法
- this.doJob();
-
- // 提交事务
- TransactionManager.commit();
- } catch (Exception e) {
- // 出错了, 回滚事务
- TransactionManager.rollback();
- } finally {
- // 关闭连接
- TransactionManager.close();
- }
- }
-
- protected abstract void doJob() throws Exception; // 具体的业务用户自己实现
- }
在TransactionTemplate类中定义一个doJobInTransaction方法,在该方法中首先使用TransactionManager开始事务,
然后调用doJob方法完成业务功能,doJob方法为抽象方法,完成业务功能的子类应该实现该方法,
最后,根据doJob方法执行是否成功决定commit事务或是rollback事务。
2. 改造业务处理类AccountService
[java] view plain copy
- /**
- * 业务逻辑层
- */
- public class AccountService {
-
- public void transfer(final Account out, final Account in, final int money) throws SQLException {
-
- new TransactionTemplate() {
- @Override
- protected void doJob() throws Exception {
- // 查询两个账户
- AccountDAO accountDAO = new AccountDAO();
- Account outAccount = accountDAO.findAccountById(out.getId());
- Account inAccount = accountDAO.findAccountById(in.getId());
-
- // 转账 - 修改原账户金额
- outAccount.setMoney(outAccount.getMoney() - money);
- inAccount.setMoney(inAccount.getMoney() + money);
-
- // 更新账户金额
- accountDAO.update(outAccount);
- accountDAO.update(inAccount);
- }
- }.doJobInTransaction();
-
- }
- }
在AccountService的transfer方法中,我们创建了一个匿名的TtransactionTemplate类,并且实现了doJob方法(doJob方法中两次调用DAO完成业务操作),然后调用调用TransactionTemplate的doJobInTransaction方法。doJobInTransaction内部帮我们调用doJob并实现事务控制
参考资料:http://www.davenkin.me/post/2013-02-23/40049802880
源码下载: http://download.csdn.net/detail/zdp072/7908279
一. 前言:
在上一篇博文中, 我们使用模板模式进行事务管理, 代码看起来已经很简洁了, 但是还是不太完美,
我们依然需要在service层编写和事务相关的代码, 即我们需要在service层宗声明一个TransactionTemplate.
本篇文章中, 我们将使用Java提供的动态代理来完成事务处理, 你将看到无论在service层还是在dao层都不会
有事务处理代码
二. 例子:
1. 代码结构图:
2. TransactionProxy
[java] view plain copy
- /**
- * 动态代理
- */
- public class TransactionProxy {
-
- public static Object proxyFor(Object object) {
- return Proxy.newProxyInstance(
- object.getClass().getClassLoader(),
- object.getClass().getInterfaces(),
- new TransactionInvocationHandler(object)
- );
- }
- }
-
- class TransactionInvocationHandler implements InvocationHandler {
- private Object proxy;
-
- TransactionInvocationHandler(Object object) {
- this.proxy = object;
- }
-
- public Object invoke(Object obj, Method method, Object[] objects) throws Throwable {
- TransactionManager.beginTransaction();
- Object result = null;
- try {
- // 调用业务方法
- result = method.invoke(proxy, objects);
- TransactionManager.commit();
- } catch (Exception e) {
- TransactionManager.rollback();
- } finally {
- TransactionManager.close();
- }
- return result;
- }
- }
拦截service层的transfer方法, 在调用之前加入事务准备工作, 然后调用原来的transfer方法,
之后根据transfer方法是否执行成功决定commit还是rollback
3. 接口类AccountService
[java] view plain copy
- /**
- * 业务逻辑层接口
- */
- public interface AccountService{
- public void transfer(Account outAccount, Account inAccount, int money) throws SQLException;
- }
使用动态代理, 被代理类和代理类必须要实现相同的接口
4. 业务实现类AccountServiceImpl
[java] view plain copy
- /**
- * 业务逻辑层
- */
- public class AccountServiceImpl implements AccountService {
-
- @Override
- public void transfer(Account outAccount, Account inAccount, int money) throws SQLException {
- // 查询两个账户
- AccountDAO accountDAO = new AccountDAO();
- outAccount = accountDAO.findAccountById(outAccount.getId());
- inAccount = accountDAO.findAccountById(inAccount.getId());
-
- // 转账 - 修改原账户金额
- outAccount.setMoney(outAccount.getMoney() - money);
- inAccount.setMoney(inAccount.getMoney() + money);
-
- // 更新账户金额
- accountDAO.update(outAccount);
- accountDAO.update(inAccount);
- }
- }
5. 测试类:
[java] view plain copy
- public class TransferTest {
-
- @Test
- public void transferTest() throws SQLException {
- Account out = new Account();
- out.setId(1);
-
- Account in = new Account();
- in.setId(2);
-
- AccountService accountService = new AccountServiceImpl();
-
- // 获取accountService代理
- AccountService accountServiceProxy = (AccountService) TransactionProxy.proxyFor(accountService);
- accountServiceProxy.transfer(out, in, 100);
- }
- }
调用proxyFor方法, 传入需要被代理的对象, 返回一个代理对象, 代理对象条用transfer方法会被加入事务处理
三. 总结:
通过动态代理, AccountServiceImpl中所有public方法都被代理了, 即它们都被加入事务中, 这对于service层中所有方法都需要和数据库打交道的情况是可以的, 然而对于service层中不需要和数据库打交道的public方法, 这样做虽然不会报错, 但是却显得多余.
参考文章: http://www.davenkin.me/post/2013-02-24/40049235086
源码下载: http://download.csdn.net/detail/zdp072/7908797