JDBC_事务管理
2017-06-03 本文已影响44人
明天你好向前奔跑
一、什么是事务
逻辑上的一组操作, ,要不全部失败,要不全部成功。
对数据库的操作会先在一张临时表上进行,一旦发生错误,就回滚撤销所有操作。全部成功才存盘修改数据库数据。
MySql的事务管理
1. 启动事务(对数据的操作在临时表中进行)
startTransaction
2. 提交,存盘
commit
3. 回滚,撤销
rollBack
- 在事务管理中执行sql,使用数据库内临时表保存,在没有进行事务提交或者回滚之前,其它用户无法看到事务操作的结果
- SQL语言中只有DML才能被事务管理(insert/update/delete)
JDBC的事务
1. 启动事务管理
//首先获取连接。(以C3P0为基础准备一个JDBCUtils工具类,提供获取连接池和连接的方法)
Connection connection = JDBCUtils.getConnection();
//启动事务
connection.setAutoCommit(false);
2. 提交,存盘
connection.commit();
3. 回滚,撤销
connection.rollBack();
DBUtils的事务管理
1. 启动事务
//创建QuueryRunner对象,使用它的无参构造方式,自己给连接。从而可以通过连接控制事务
QueryRunner runner = new QueryRunner();
Connection connection = JDBCUtils.getConnection();
connection.setAutoCommit(false);
2. 提交并释放资源,异常由框架处理(其实没处理)
//connection.commitAndCloseQuietly();
3. 回滚,撤销并释放资源,异常由框架处理(其实没处理)
//connection.rollbackAndCloseQuietly();
事务回滚点 SavePoint(类似与游戏中的存档)
当事务特别复杂,有些情况不会回滚到事务的最开始状态,需要将事务回滚到指定位置
* 核心API
* connection.setSavepoint();// 设置回滚点
* connection.rollback(savepoint);//事务回滚到指定回滚点
- 示例代码
- 往数据库中添加数据,每1000条保存一次。
Connection connection = null;
PreparedStatement prepareStatement = null;
Savepoint savepoint = null;
try {
connection = JDBCUtils.getConnection();
// 开启事务
connection.setAutoCommit(false);
// 设置初始回滚点
savepoint = connection.setSavepoint();
String sql = "insert into person values (?,?)";
prepareStatement = connection.prepareStatement(sql);
for (int i = 1; i <= 5000; i++) {
prepareStatement.setInt(1, i);
prepareStatement.setString(2, "name" + i);
prepareStatement.addBatch();
// 模拟错误
if (i == 3201) {
int x = 11 / 0;
}
// 每隔200条数据执行一次批处理
if (i % 200 == 0) {
prepareStatement.executeBatch();
prepareStatement.clearBatch();
}
if (i % 1000 == 0) {
// 每一千条数据,创建一个回滚点
savepoint = connection.setSavepoint();
}
}
prepareStatement.executeBatch();
prepareStatement.clearBatch();
// 没有异常,提交事务
connection.commit();
} catch (Exception e) {
try {
// 发生异常,事务回滚到指定回滚点
connection.rollback(savepoint);
// 提交事务
connection.commit();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
JDBCUtils.release(connection, prepareStatement);
}
二、事务案例:简易转账案例
1、分析
![](https://img.haomeiwen.com/i5303154/31f85957dac8ab20.png)
2、搭建环境
1. 数据库及用户表单的创建。
CREATE DATABASE day20;
USE day20;
CREATE TABLE USER(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(40),
money DOUBLE
);
INSERT INTO USER VALUES(NULL,'tom',2000);
INSERT INTO USER VALUES(NULL,'jerry',2000);
2. 数据库驱动,c3p0的jar包,c3p0.xml配置文件,dbutils的jar包,
3. 转账页面
<form action="${pageContext.request.contextPath}/transfer" method="post">
转账人:<input type="text" name="sender" /><br/>
被转账人:<input type="text" name="receiver" /><br/>
转账金额:<input type="text" name="acount" /><br/>
<input type="submit" value="转账"> <br/>
</form>
3、分包实现
3.1、创建web层TransactServlet
Servlet要做的三件事:
- 获取表单提交的参数
- 调用业务逻辑
- 分发转向
public class TransferServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置编码集
response.setContentType("text/html;charset=UTF-8");
try {
// 获取参数
String sender = request.getParameter("sender");
String receiver = request.getParameter("receiver");
String amount = request.getParameter("amount");
// 调用业务层对象,执行转账
TransferService service = new TransferService();
service.transfer(sender, receiver, amount);
// 写出响应
response.getWriter().write("转账成功");
} catch (Exception e) {
// 写出响应
response.getWriter().write("转账失败:" + e.getMessage());
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
3.2、创建service层TransferService
与数据库交互的操作交到dao层完成,service层只处理业务逻辑
public class TransferService {
public void transfer(String sender, String receiver, String amount) throws Exception {
Connection connection = null;
try {
// 与数据库交互的事情交给dao层去操作 ,创建dao对象
TransferDao dao = new TransferDao();
// 开启事务。要想控制事务dao层的connection连接对象和这里的连接对象应该一致.因此将这个connection对象作为参数传递下去。
//connection = JDBCUtils.getConnection();
//connection.setAutoCommit(false);
//为了不打破分层结构,不应该将连接Connection的操作放在Service层.因此抽取出来
JDBCUtils.startTransaction();
// 改变的行数
int outDao = dao.outDao(sender, amount);
//一旦数据库操作失败,影响行数即为0,抛出异常带上对应的信息。
if(outDao != 1) {
throw new RuntimeException("转出失败");
}
int inDao = dao.inDao(receiver, amount);
if(inDao != 1) {
throw new RuntimeException("转入失败");
}
// 没有异常,提交事务
JDBCUtils.commitAndRelease();
//这里是用try-catch是为了保证有异常与无异常时的两种处理方式
//这里抓住了上面抛出的转账信息的异常但是为了告诉Servlet这里发生了异常,将该异常继续向上抛
} catch (Exception e) {
// 发生异常,回滚撤销操作
JDBCUtils.rollbackAndRelease();
throw e;
}
}
}
3.3、创建utils工具类JDBCUtils
提供与数据库操作相关的工具:获取连接池与获取连接的方法
因为DBUtils框架的QueryRunner对象会自行释放资源,因此不提供释放Connection等资源的方法。
private static DataSource dataSource = new ComboPooledDataSource();
// 获取连接池对象
public static DataSource getDataSource() {
return dataSource;
}
// 提供连接
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
3.4、创建dao层TransferDao
进行操作数据库的动作。
public class TransferDao {
public int outDao(String sender, String amount) throws SQLException {
//对数据库数据进行更改
//1.创建QueryRunner对象
QueryRunner runner = new QueryRunner();
Connection connection = JDBCUtils.getConnectionTL();
//2.执行sql语句
String sql = "update user set money=money-? where name=?";
//为了告知service层sql语句是否执行成功,返回被影响的行数
int i = runner.update(connection, sql, amount,sender);
return i;
}
public int inDao(String receiver, String amount) throws SQLException {
//对数据库数据进行更改
//1.创建QueryRunner对象
QueryRunner runner = new QueryRunner(JDBCUtils.getDataSource());
Connection connection = JDBCUtils.getConnectionTL();
//2.执行sql语句
String sql = "update user set money=money+? where name=?";
int i = runner.update(connection, sql, amount,receiver);
return i;
}
}
3.5、优化代码,完善utils工具类
-
因为第一版中service层执行了属于dao层与数据库交互的操作,这打破了三层结构,为了避免这种情况,应当考虑将与数据库交互的相关操作抽取出来,于是我将控制事务的代码抽取到工具类中。提供开启事务,提交事务和回滚事务的方法。
-
但是为了控制事务,service层和dao层的connection应当是同一个connection,因此使用了一个类似于map集合的ThreadLocal类,它的内部存在<Thread.currentThread,Connection>的键值对,通过它保证获取同一个connection连接。
public class JDBCUtils {
private static DataSource dataSource = new ComboPooledDataSource();
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
// 获取连接池对象
public static DataSource getDataSource() {
return dataSource;
}
// 提供连接
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// 提供连接,从ThreadLocal中获取的
public static Connection getConnectionTL() throws SQLException {
// 从ThreadLocal中获取连接
Connection connection = threadLocal.get();
// 如果连接对象为空
if (connection == null) {
// 从连接池获取连接
connection = getConnection();
// 将连接放入ThreadLocal,这样下一次从ThreadLocal中获取的时候就有数据了
threadLocal.set(connection);
}
return connection;
}
// 开启事务
public static void startTransaction() throws SQLException {
Connection connection = getConnectionTL();
connection.setAutoCommit(false);
}
// 提交事务并释放资源
public static void commitAndRelease() throws SQLException {
Connection connection = getConnectionTL();
// 提交事务
connection.commit();
if (connection != null) {
connection.close();
connection = null;
}
// 把连接对象和ThreadLocal解绑,可以让ThreadLocal尽快被释放,节约服务器内存资源
threadLocal.remove();
}
// 回滚事务并释放资源
public static void rollbackAndRelease() throws SQLException {
Connection connection = getConnectionTL();
// 回滚事务
connection.rollback();
if (connection != null) {
connection.close();
connection = null;
}
// 把连接对象和ThreadLocal解绑,可以让ThreadLocal尽快被释放,节约服务器内存资源
threadLocal.remove();
}
}
3.6、 案例实现_为什么要使用事务?
-
如果不考虑事务,会导致转账钱丢失的问题.所以必须考虑事务
-
如果要避免上述问题,只需要保证所有的操作使用同一个连接对象即可
-
解决方法
- 传递参数Connection(打破了三层架构,因此采用线程绑定)
- 线程绑定ThreadLocal
-
ThreadLocal简介
- 作用 : 把一个操作对象和当前线程绑定在一起. 其内部维护了一个Map集合.key就是当前线程,value就是要绑定的内容
- 常用API
- set(T value) : 把一个对象和当前线程进行绑定.等价于Map.put(Thread.currentThread(),value)
- T get() : 获取和当前线程绑定在一起的对象.等价于Map.get(Thread.currentThread())
- remove() : 移除和当前线程绑定在一起的对象.等价于Map.remove(Thread.currentThread())
![](https://img.haomeiwen.com/i5303154/316e443a63b42a14.png)
![](img03.png)
4、事务的一些概念
事务特性
-
<font color='red'>事务的四大特性:
- 原子性(Atomicity):事务的一组操作不可分割,要么都成功,要么都失败
- 一致性(Consistency):事务前后数据保持完整性.转账前A和B账户总和2000元,转账后总和还是2000 元
- 隔离性(Isolation):并发访问时,事务之间是隔离的,一个事务不应该影响其它事务的运行效果
- 持久性(Durability):当事务一旦提交,事务数据永久存在,无法改变 </font>
-
企业开发中一定要保证事务原子性
-
事务最复杂问题都是由事务隔离性引起的
隔离性
-
不考虑事务隔离将引发的问题
- 脏读:一个事务读取另一个事务未提交的数据.这是数据库隔离中最重要的问题
- 不可重复读:一个事务读取另一个事务已提交的数据,在一个事务中两次查询结果不同(针对update操作)
- 虚读:一个事务读取另一个事务插入的数据,造成在一个事务中两次查询记录条数不同(针对insert操作)
-
数据库为了解决三类隔离引发问题,提供了四个数据库隔离级别(所有数据库通用)
- <font color='red'> Serializable : 串行处理.可以解决三类问题
- Repeatable read :可以解决不可重复读、脏读,但是会发生虚读.是MySQL的默认级别
- read committed : 可以解决脏读,会发生不可重复读、虚读.是Oracle的默认级别
- read uncommitted : 会导致三类问题发生 </font>
- 按照隔离级别从高到低排序 : Serializable > Repeatable read > read committed > read uncommitted
- 数据库隔离问题危害的排序 : 脏读> 不可重复读 > 虚读
- 多数数据库厂商都会采用Repeatable read或read committed两个级别.
-
更改事务隔离级别的语句
- set transaction isolation level 设置事务隔离级别
- select @@tx_isolation; 查询当前事务隔离级别
隔离级别引发问题的小实验
-
脏读问题(read uncommitted)
- 开启两个窗口,执行一次查询,获得一个结果
- 将B窗口隔离级别设置为read uncommitted
- set session transaction isolation level read uncommitted;
- 在A、B窗口分别开启一个事务 start transaction;
- 在A窗口完成转账操作
- update account set money= money - 200 where name='aaa';
- update account set money= money +200 where name='bbb';
- 在B窗口进行查询,会读取到A窗口未提交的转账结果
- A窗口进行回滚rollback, B窗口查询结果恢复之前
-
不可重复读(read committed)
- 开启两个窗口,执行一次查询,获得一个结果
- 将B窗口隔离级别设置为read committed
- set session transaction isolation level read committed;
- 在A、B窗口分别开启一个事务 start transaction;
- 在A窗口完成转账操作
- update account set money= money - 200 where name='aaa';
- update account set money= money +200 where name='bbb';
- 此时在B窗口执行查询操作,数据不会发生改变.避免了脏读问题
- A窗口执行commit,B窗口再次执行查询,会读取到A窗口提交的结果.注意此时B窗口没有提交事务,也就是在同一事务中,读取到了两个结果.发生不可重复读问题
-
虚读(Repeatable read)
- 开启两个窗口,执行一次查询,获得一个结果
- 将B窗口隔离级别设置为Repeatable read
- set session transaction isolation level repeatable read;
- 在A、B窗口分别开启一个事务 start transaction;
- 在A窗口完成转账操作
- update account set money= money - 200 where name='aaa';
- update account set money= money +200 where name='bbb';
- 此时在B窗口执行查询操作,数据不会发生改变.避免了脏读问题
- A窗口执行commit,B窗口再次执行查询,数据仍然不会发生改变.避免了不可重复读.
- 此时如果在A窗口插入一条数据,而B窗口可以查询到,就是发生了虚读问题.但是这种情况发生的几率非常小.
-
Serializable
- 开启两个窗口,执行一次查询,获得一个结果
- 将B窗口隔离级别设置为read serializable
- set session transaction isolation level serializable;
- 在A、B窗口分别开启一个事务 start transaction;
- 在B窗口执行查询操作
- 在A窗口执行插入操作.此时A窗口将会被卡住,不会执行语句.直到B窗口提交或回滚,释放数据库资源
- 在JDBC中,可以通过Connection.setTransactionIsolation(int level) 来设置隔离级别.如果没有设置.会采用数据库的默认级别
丢失更新问题和悲观锁乐观锁机制【了解】
-
事务丢失更新问题 : 两个事务同时读取同一条记录,A先修改记录,B也修改记录(B不知道A修改过),B提交数据后B的修改结果覆盖了A的修改结果。
-
解决丢失更新的两种方式
- 事务和锁是不可分开的,锁一定是在事务中使用 ,当事务关闭锁自动释放
- 悲观锁
- 假设丢失更新会发生
- 使用数据库内部锁机制,进行表的锁定,在A修改数据时,A就将数据锁定,B此时无法进行修改
- 在mysql中默认情况下,当你修改数据,自动为数据加锁(在事务中),防止两个事务同时修改数据
- 在mysql内部有两种常用锁
- 读锁(共享锁)
- 一张表可以添加多个读锁,如果表被添加了读锁(不是当前事务添加的),该表不可以修改
- 语法 : select * from account lock in share mode;
- 共享锁非常容易发生死锁
- 写锁(排它锁)
- 一张表只能加一个排它锁,排他锁和其它共享锁、排它锁都具有互斥效果 。
- 如果一张表想添加排它锁,前提是之前表一定没有加过共享锁和排他锁
- 语法 : select * from account for update ;
- 读锁(共享锁)
- 乐观锁
- 假设丢失更新不会发生
- 使用的不是数据库锁机制,而是一个特殊标记字段 : 数据库timestamp 时间戳字段