主要内容:
事务
使用数据库连接池优化程序性能
一、事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功。数据库开启事务命令:
start transaction 开启事务
rollback 回滚事务
commit 提交事务
当我们开启事务后,可输入多条sql语句让数据库执行,但是如果我们在让sql语句执行之后最后没有使用commit提交事务,则前面执行的所有sql语句无效,这就相当于回到了开启事务之前的状态,当然有时候这种方式并不太好,我们可以自己设置回滚点,当我们sql语句出错时可以回到设置的那个点处的状态。而rollback可以每次回滚一条语句。
二、使用事务
当jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它前面发送的sql语句。若向关闭这种默认提交方式,让多条sql在一个事务中执行,可使用下列语句:
jdbc控制事务语句
connection.setAutoCommit(false); start transaction
connection.rollback(); rollback
connection.commit(); commit
设置事务回滚点
Savepoint sp = conn.setSavepoint();
conn.rollback(sp);
conn.commit(); //回滚后必须要提交
例:
创建数据库:
create database day16;
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(40),
money FLOAT
)CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO account(NAME,money) VALUES('aaa',1000);
INSERT INTO account(NAME,money) VALUES('bbb',1000);
INSERT INTO account(NAME,money) VALUES('ccc',1000);
Demo1.java
package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import cn.itcast.utils.JdbcUtils;
public class Demo1 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet result = null;
try {
conn = JdbcUtils.getConnection();
String sql1 = "update account set money=money-100 where name='aaa'";
String sql2 = "update account set money=money+100 where name='bbb'";
conn.setAutoCommit(false);//开启事务
ps = conn.prepareStatement(sql1);
ps.executeUpdate();
//int x = 1/0;//模拟异常,sql语句不会执行
ps = conn.prepareStatement(sql2);
ps.executeUpdate();
System.out.println("ok");
} catch (Exception e) {
try {
conn.rollback();//手动通知数据库手动回滚
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn, ps, result);
}
}
}
例:设置回滚点
Demo2.java
package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import cn.itcast.utils.JdbcUtils;
public class Demo2 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet result = null;
Savepoint point = null;
try{
conn = JdbcUtils.getConnection();
String sql1 = "update account set money = money - 100 where name = 'aaa'";
String sql2 = "update account set money = money + 100 where name = 'bbb'";
String sql3 = "update account set money = money + 100 where name = 'ccc'";
conn.setAutoCommit(false);
ps = conn.prepareStatement(sql1);
ps.executeUpdate();
point = conn.setSavepoint();//设置回滚点
ps = conn.prepareStatement(sql2);
ps.executeUpdate();
//int x = 1/0;
ps = conn.prepareStatement(sql3);
ps.executeUpdate();
conn.commit();
}catch(Exception e){
try {
conn.rollback(point);//手动通知回滚,同时指定回滚点
//回滚之后记得提交,上面我们回滚了,就表明最后的提交语句没有执行,那此时如果不提交
//,数据库在没有收到提交的情况下,会自动回滚所有的sql语句
conn.commit();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn, ps, result);
}
}
}
说明:这里如果中间出现异常,则只有第一条语句生效。
三、事务的特性(ACID)
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都是执行成功,要么都失败。
一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。比如,在转账中账户的总额是不变的。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
四、 事务的隔离级别
多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
如果不考虑隔离性,可能会引发如下问题:
脏读
指一个事务读取了另外一个事务未提交的数据。
例如:a花钱让b给办点事,a向b转账之后但未提交,但是b此时会发现账户多了钱,然后将事办完之后a却不提交,此时b的账户的钱就会变回原来的数目,相当于白干活了。
不可重复读
在一个事务内读取表中的某一行数据,多次读取的结果不同。
如a开启一个事务后,查询余额为200,此时b转账100,那么a此时查询就是300,两次结果不一致。当然有些时候这样是正确的,但是有时候却不是,如在统计时我们不能让多次的统计结果不一致。
和脏读的区别:脏读是读取前一事务未提交的数据,不可重复读是重新读取了前一事务已经提交的数据。
虚读(幻读)
是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
比如一个表第一次查询有2条数据,此时另外一个事务插入了一条数据,此时再次查询就变成了3条数据,两次查询结果不一致。
和不可重复读的区别:不可重复读是读取到的数据结果不同,而虚读是指读取到多个事务导致结果不一致。
五、事务隔离性的设置语句
数据库共定义了四种隔离级别:
Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)
Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读)
Read committed:可避免脏读情况发生(读已提交)。
Read uncommitted:最低级别,以上情况均无法保证。(读未提交)
set transaction isolation level设置事务隔离级别(数据库操作)
select @@tx_isolation查询当前事务隔离级别(数据库操作)
例:设置隔离级别
Demo3.java
package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import cn.itcast.utils.JdbcUtils;
public class Demo3 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet result = null;
try {
conn = JdbcUtils.getConnection();
//查询程序肯定至少要到这个级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setAutoCommit(false);
String sql = "select * from account where name='aaa'";
ps = conn.prepareStatement(sql);
result = ps.executeQuery();
if(result.next()){
System.out.println(result.getFloat("money"));
}
Thread.sleep(1000*10);
result = ps.executeQuery();
if(result.next()){
System.out.println(result.getFloat("money"));
}
} catch (Exception e) {
e.printStackTrace();
}finally{
JdbcUtils.release(conn, ps, result);
}
}
}
六、使用数据库连接池优化程序性能
应用程序直接获取连接的缺点:用户每次都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。
1.png
这时我们可以使用数据库连接池优化程序性能:
2.png
编写连接池需要实现java.sql.DataSource接口。DataSource接口中定义了两个重载的getConnection方法:
Connection getConnection()
Connection getConnection(String username,String password)
实现DataSource接口,并实现连接池功能的步骤:
1.在DataSource构造函数中批量创建与数据库的连接,并把创建的连接加入LinkedList对象中。
2.实现getConnection方法,让getConnection方法每次调用时,从LinkedList中取一个Connection返回给用户。
3.当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到LinkedList中,而不要把Collection还给数据库。
Collection保证将自己返回到LinkedList中是此处编程的难点。
示例:模拟数据库连接池
JdbcPool.java
package junit.test;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.LinkedList;
import java.util.Properties;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class JdbcPool implements DataSource {
//后面涉及到大量的增删改查,所以使用LinkedList类
private static LinkedList list = new LinkedList();
static {
try {
InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
Properties properties = new Properties();
properties.load(in);
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
String username = properties.getProperty("username");
String password = properties.getProperty("password");
Class.forName(driver);
for(int i = 0; i < 10; i++){
Connection conn = DriverManager.getConnection(url, username, password);
list.add(conn);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Connection getConnection() throws SQLException {
if(list.size() > 0){
//如果连接池中还有Connection连接则从池中删除并返回给调用者
Connection conn = list.removeFirst();
return conn;
}else{
throw new RuntimeException("数据库正忙");
}
}
其他需要实现的方法我们并不关心,这里省略
}
说明:
这里有个问题是如果调用者使用完Connection链接之后调用方法conn.close();那么此链接将不会返回给数据库连接池,而是直接返回给了数据库,这样链接会用一个少一个,显然不行。也就是说不能这样,或者说close方法不够用,我们需要增强一下,让其不要返还给数据库,而是返还给连接池。
对于类的某个方法不能达到我们的要求时需要对其进行增强,而增强的方式有三种:1.写一个子类,覆盖其close方法;2.写一个Connection的包装类,增强close方法;3.使用动态代理,返回一个代理对象出去,拦截close方法的调用,达到对close方法增强的功能。
第一种不行,因为我们在要返回Connection的时候对象中已经封装了相关信息,即使我们写一个子类也仅仅表明此子类有和父类相同的功能,但是却没有父类中已经封装好了的信息,不能对数据库进行操作。
第二种:写一个包装类。
/*
* 用包装设计模式对某个对象进行增强步骤:
* 1、写一个类,实现与被增强对象(这里要增强的对象是mysql的连接对象connection)相同的接口(这里的接口是Connection)
* 2.定义一个变量,指向被增强对象
* 3、定义一个构造方法,接收被增强对象(也就是将我们要增强的对象传递进来进行增强)
* 4、覆盖想增强的方法(这里是close方法)、
* 5、对于不想增强的方法,直接调用被增强对象的方法,如this.conn.unwrap(iface)
* */
class MyConnection implements Connection{
private Connection conn ;
private List pool;//这里我们需要将数据库连接池传递进来,因为之后我们使用的链接都是增强之后的链接
public MyConnection() {
}
public MyConnection(Connection conn , List pool){
this.conn = conn;
this.pool = pool;
}
//这里我们只是需要增强close方法,其他方法直接调用父类的方法即可
@Override
public void close() throws SQLException {
pool.add(conn);
}
@Override
public T unwrap(Class iface) throws SQLException {
return this.conn.unwrap(iface);
}
其他方法和上面这个方法类似,此处省略
}
说明:之后返回就不是返回Connection对象了,而是return new MyConnection(conn, list);,但是显然我们可以看到这种方式太麻烦,因为其中的方法太多。
第三种:使用动态代理
@Override
public Connection getConnection() throws SQLException {
if(list.size() > 0){
//如果连接池中还有Connection连接则从池中删除并返回给调用者
final Connection conn = list.removeFirst();
//第一个参数指的是使用哪个类装载器,第二个参数指明我们要对那个对象进行增前,第三个参数指明增强对象完成什么功能
return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(),
new InvocationHandler() {
//使用动态代理之后其实不管之后我们调用Connection的什么方法(commit、rollback...)其实都是调用下面的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
//如果调用的方法不是close方法,那么我们使用原来Connection的方法
if(!method.getName().equals("close")){
return method.invoke(conn, args);
}else{
//如果调用close方法,我们将链接返还给数据库连接池
return list.add(conn);
}
}
});
}else{
throw new RuntimeException("数据库正忙");
}
}
说明:其实动态代理是使用的拦截技术,这里我们不详细讲,在后面将过滤器会详细说明。
那么我们可以对之前的数据库工具类做一些改进:
JdbcUtils.java
package junit.test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcUtils {
private static JdbcPool pool = new JdbcPool();
public static Connection getConnection() throws SQLException{
return pool.getConnection();
}
public static void release(Connection conn, Statement ps , ResultSet result){
if(result != null){
try {
result.close();
} catch (Exception e) {
e.printStackTrace();
}
result = null;
}
if(ps != null){
try {
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
ps = null;
}
if(conn != null){
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
conn = null;
}
}
}