事务
Transaction 指的是一组操作,有很多单个逻辑,只要有一个逻辑执行失败,那么整个事务失败,所有数据回归到一开始的数据。
- 使用命令行演示事务
-
开启事务
start transaction
-
提交或者回滚事务
commit提交事务 rollback回滚事务
-
关闭自动提交功能
set autocommit = off
-
演示事务
事务只是针对连接对象
- 代码演示
public static void main(String[] args){
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = JDBCUtils.getConnection();
conn.setAutoCommit(false);//关闭事务的自动提交
String sql = "update tb_bank set money = money - ? where id = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setDouble(1, 200);
pstmt.setString(2, "001");
pstmt.executeUpdate();
pstmt.setDouble(1, -200);
pstmt.setString(2, "002");
pstmt.executeUpdate();
conn.commit();//提交事务
} catch (Exception e) {
try {
conn.rollback();//回滚事务
} catch (Exception e2) {
e2.printStackTrace();
}
e.printStackTrace();
}finally {
JDBCUtils.close(conn, pstmt);
}
}
事务的特性(保证安全)
一个有效的事务处理系统必须满足(ACID特性):
- 原子性
一个事务必须被视为一个单独的内部不可分的工作单元,事务要么全部执行,要么回滚。这时事务绝对不会执行部分,不会出现A给B转账,B收款部分不执行,导致A失去了钱,B却没有得到。 - 一致性
数据库总是从一种一致性状态转换到另一种一致性状态。即使中间执行的语句崩溃了,但因为表单不会提交而无伤大雅。 - 隔离性
某个事务只有在完成后,结果才会被其他事务可见。 - 持久性
事务所做的数据改变是永久的。
一个支持ACID特性的数据库会比不支持的数据库要求对于CPU处理能力大、更大内存、更多磁盘空间 。
事务安全问题与隔离级别
- 安全问题
-
读
a. 脏读一个事务读到另一个还未提交的事务
b.不可重复读
一个事务读到了另外一个事务提交的数据 ,造成了前后两次查询结果不一致。对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
c.幻读
一个事务读到了另一个事务insert的数据,造成了查询结果不一致。
一个事务第一次查询时,查到的数据结果,与第二次查询的结果不一致,像是看错了一样。 -
写
丢失更新两个事务查询数据库数据,存入对应的缓存中(均未提及),当a事务修改数据后(由于隔离性,b事务不知道a对它修改了),a事务提交,数据库数据发生第一次改变,之后b事务在原数据(缓存中数据)上修改,提交b事务,则会覆盖a事务的结果,造成a事务更新数据失败,即丢失更新
-
可串行化
如果有一个连接的隔离级别设置为了串行化 ,那么谁先打开了事务, 谁就有了先执行的权利, 谁后打开事务,谁就只能得着,等前面的那个事务,提交或者回滚后,才能执行。 但是这种隔离级别一般比较少用。 容易造成性能上的问题。 效率比较低。
MySQL数据库的四种隔离级别(拦截级别由高到低):
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。(级别最高)
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。(级别最低)四种隔离级别效率由高到低:
Read uncommitted (读未提交) > Read committed (读已提交) > Repeatable read (可重复读) > Serializable (串行化)
mySql 默认的隔离级别是 可重复读
Oracle 默认的隔离级别是 读已提交
解决丢失更新
-
悲观锁
在查询语句后面加上 for update(数据库锁机制)select * from tb_stu for update
排他锁(写锁)是悲观锁的一种实现。
排他锁:在一定时间内,只有一个用户可以写入资源,会阻塞其他读锁和写锁。 -
乐观锁
A事务在数据库取出数据,其中有一个version,B事务也同样有一个version,两者一开始是相同的。A事务先操作完成提交给数据库,version改变;B事务开始操作,在提交到数据库前先比对自己的version和数据库的version是否一致,如果不一致则取消提交,更新数据,若一致则提交。
数据库连接池
- 数据库的连接对象创建工作,比较消耗性能。
- 一开始现在内存中开辟一块空间(集合) , 一开始先往池子里面放置 多个连接对象。 后面需要连接的话,直接从池子里面去。不要去自己创建连接了。 使用完毕, 要记得归还连接。确保连接对象能循环利用。
自定义连接池
- 代码实现
/**
* 这是一个数据库连接池
* 一开始先往池子放上10个连接
* 来的程序通过getConnection方法获取连接
* 用完之后通过addBack方法归还连接
* 扩容
*/
public class MyDataSource implements DataSource{
List<Connection> list = new ArrayList<Connection>();
public MyDataSource() {
for(int i = 0; i < 10; i++) {
Connection conn = JDBCUtil.getConn();
list.add(conn);
}
}
//该连接池对外公布的获取连接的方法
@Override
public Connection getConnection() throws SQLException {
if(0 == list.size()) {//连接池没有了
for(int i = 0; i < 5; i++) {
Connection conn = JDBCUtil.getConn();
list.add(conn);
}
}
Connection conn = list.remove(0);
return conn;
}
public void addBack(Connection conn) {
list.add(conn);
}
}
- 解决自定义数据库连接池出现的问题
因为多了一个addBack()方法,导致无法面向接口编程,因为接口里没有这个方法,使用这个连接池的地方需要你额外记住这个方法。
我们打算修改原来的JDBCUtils的close方法,让它成为归还连接对象的方法,而不是真的关闭连接。
装饰者模式
ConnectionWrap:
public class ConnectionWrapper implements Connection{
Connection conn = null;
List<Connection> list;
public ConnectionWrapper(Connection conn, List<Connection> list) {
super();
this.conn = conn;
this.list = list;
}
@Override
public void close() throws SQLException {
System.out.println("有人来归还了,归还前是" + list.size());
list.add(conn);
System.out.println("有人来归还了,归还后是" + list.size());
}
//......
}
之后在MyDataSource 里返回的是装饰过的ConnectionWrapper 类,测试类中只需要conn.close()就可以,这时候的close不是关闭连接,而是ConnectionWrapper里的close归还连接。
开源连接池
- DBCP
public void testDBCP01() {
Connection conn = null;
PreparedStatement pstmt = null;
try {
//1.创建数据源对象
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/bank_db");
dataSource.setUsername("root");
dataSource.setPassword("root");
//2.得到连接对象
conn = dataSource.getConnection();
String sql = "insert into tb_bank values ('004', 'admin', 3000)";
pstmt = conn.prepareStatement(sql);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtil.release(conn, pstmt);
}
}
配置法:
@Test
public void testDBCP01() {
Connection conn = null;
PreparedStatement pstmt = null;
try {
BasicDataSourceFactory dataFactory = new BasicDataSourceFactory();
Properties properties = new Properties();
InputStream is = getClass().getClassLoader().getResourceAsStream("dbcpconfig.properties");
properties.load(is);
DataSource dataSource = dataFactory.createDataSource(properties);
conn = dataSource.getConnection();
String sql = "insert into tb_bank values ('005', 'adward', 30000)";
pstmt = conn.prepareStatement(sql);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.release(conn, pstmt);
}
}
- C3P0
方法一:
public void testC3P0() {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
Connection conn = null;
PreparedStatement pstmt = null;
try {
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/bank_db");
dataSource.setUser("root");
dataSource.setPassword("root");
conn = dataSource.getConnection();
String sql = "insert into tb_bank values ('006', 'bella', 9000)";
pstmt = conn.prepareStatement(sql);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.release(conn, pstmt);
}
}
方法二:推荐使用C3P0的配置方法
在src下导入c3p0-config.xml,一定要是这个名字。同方法一同样,只不过删除dataSource.setxxx就行
c3p0-config配置:
这个时候JDBCUtils就可以更新了,除了close类型的方法全部删除,添加如下代码就可以了(配合配置文件):
static ComboPooledDataSource dataSource = null;
static {
dataSource = new ComboPooledDataSource();//里面自己选择是哪个数据库,有MySQL和oracle
}
public static Connection getConnection() throws Exception {
return dataSource.getConnection();
}
DBUtils
- 增删改
queryRunner.update(sql语句,参数);
例:
try {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
//QueryRunner简化了CRUD的代码,但是不包括连接的创建以及工作获取
QueryRunner queryRunner = new QueryRunner(dataSource);
queryRunner.update("insert into tb_bank values ('008', ?, ?)", "顾溪栎", 20000);//增
} catch (SQLException e) {
e.printStackTrace();
}
- 查
- 查询一个对象
Result result2 = queryRunner.query("select * from tb_bank where id = ?",
new BeanHandler<Result>(Result.class), 3);//查询第三个
- 查询全部
List<Result> result3 = queryRunner.query("select * from tb_bank",
new BeanListHandler<Result>(Result.class));
- 直接new 接口的匿名实现类
Result result = queryRunner.query("select * from tb_bank", new ResultSetHandler<Result>() {
@Override
public Result handle(ResultSet rs) throws SQLException {
Result r = new Result();
while(rs.next()) {
String name = rs.getString("name");
double money = rs.getDouble("money");
r.setName(name);
r.setMoney(money);
}
return r;
}
});
注Result:
-
ResultSetHandler 常用的实现类
以下两个是使用频率最高的BeanHandler, 查询到的单个数据封装成一个对象 BeanListHandler, 查询到的多个数据封装 成一个List<对象>
ArrayHandler, 查询到的单个数据封装成一个数组
ArrayListHandler, 查询到的多个数据封装成一个集合 ,集合里面的元素是数组。
MapHandler, 查询到的单个数据封装成一个map
MapListHandler,查询到的多个数据封装成一个集合 ,集合里面的元素是map。
元数据
meta data
描述数据的数据String sql,描述这份sql字符串的数据叫元数据。
数据库元数据 DatabaseMetaData
参数元数据 ParameterMetaData
结果集元数据 ResultSetMetaData
CommonCRUDUtil
增删改万能代码:
//传递的数据类型不确定,所以使用Object
public void update(String sql, Object ...args) {
......
// for(int i = 0; i < args.length; i++) {
// pstmt.setObject(i + 1, args[i]);
// }
//使用元数据,获取?的个数
ParameterMetaData metaData = pstmt.getParameterMetaData();
int count = metaData.getParameterCount();
for(int i = 0; i < count; i++) {
pstmt.setObject(i + 1, args[i]);
}
......
}
查询万能代码:
由于查询,你不知道传过来的是哪一个表,所以无法封装返回,需要调用者自己封装
公共查询方法:
public <T> T query(String sql, ResultSetHandler<T> handler, Object ...args) {
......
try {
......
//把结果集发给调用者,让他自己去封装。
result = handler.hander(rs);
} catch (Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.release(conn, pstmt, rs);
}
return result;
}
public interface ResultSetHandler<T> {
/**
* 定义数据封装类型。规范
* @param rs
* @return
*/
public T hander(ResultSet rs);
}
实现类:
//封装Result类型的数据
class ResultSetHandlerImpl<T> implements ResultSetHandler<Result> {
@Override
public Result hander(ResultSet rs) {
try {
if(rs.next()) {
Result r = new Result();
String name = rs.getString("name");
double money = rs.getDouble("money");
r.setName(name);
r.setMoney(money);
return r;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
这样一来测试类只需要传递sql语句,new一个它自己要封装的类型的ResultSetHandlerImpl以及参数数据就可以。