事务 & 数据库连接池 & DBUtils
事务 & 数据库连接池 & DBUtils
事务
Transaction,其实指的一组操作,里面包含许多个单一的逻辑。
只要有一个逻辑没有执行成功,那么都算失败。 所有的数据都回归到最初的状态(回滚)。
为什么要有事务?
为了确保逻辑的成功。例子:银行的转账。
1、使用命令行方式演示事务。
(1)开启事务命令:start transaction;
(2)提交,回滚事务命令:
commit:提交事务,数据将会写到磁盘上的数据库
rollback:数据回滚,回到最初的状态。
(3)关闭自动提交功能
(4)演示事务
2、使用代码方式演示事务
代码里面的事务,主要是针对连接来的。
(1)通过conn.setAutoCommit(false) 来关闭自动提交的设置
(2)提交事务:conn.commit();
(3)回滚事务:conn.rollback();
public static void main(String[] args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtil.getConn();
//连接,事务默认就是自动提交的。 关闭自动提交。
conn.setAutoCommit(false);
String sql = "update account set money = money + ? where id = ?";
ps = conn.prepareStatement(sql);
//扣钱,扣ID为1 的100块钱
ps.setInt(1, -100);
ps.setInt(2, 1);
ps.executeUpdate();
// 模拟中间出现异常
int a = 10 / 0 ;
//加钱,给ID为2 加100块钱
ps.setInt(1, 100);
ps.setInt(2, 2);
ps.executeUpdate();
//成功: 提交事务。
conn.commit();
} catch (SQLException e) {
try {
//事变: 回滚事务
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
JDBCUtil.release(conn, ps, rs);
}
}
3、事务的特性
(1)原子性
事务中包含的逻辑,不可分割。
(2)一致性
事务执行前后,数据完整性。
语句全部执行成功,或者全部失败。
(3)隔离性
事务与事务之间没有影响,相互隔离。
(4)持久性
事务执行成功,那么数据应该持久保存到磁盘上。
4、事务的安全隐患
不考虑隔离级别设置,那么会出现以下问题。
读的问题:脏读,不可重读读,幻读。
- 脏读:A事务读到了B事务还未提交的数据。
- 不可重复读:A事务读到了B事务提交的数据,造成了前后两次查询结果不一致。
5、读未提交 演示
(1)设置A窗口的隔离级别为:读未提交
(2)AB两个窗口都分别开启事务
6、读已提交 演示
(1)设置A窗口的隔离级别为:读已提交
(2)AB两个窗口都开启事务,在B窗口执行更新操作。
(3)在A窗口两次执行的查询结果不一致。
一次是在B窗口提交事务之前,一次是在B窗口提交事务之后。
这个隔离级别能够屏蔽,脏读的现象,但是引发了另一个问题,不可重复读。
7、可串行化
(1)如果有一个连接的隔离级别设置为了串行化,那么谁先打开了事务,谁就有了先执行的权利,谁后打开事务,谁就只能得着,等前面的那个事务,提交或者回滚后,才能执行。
但是这种隔离级别一般比较少用,容易造成性能上的问题,效率比较低。
(2)按效率划分,从高到低:
读未提交 > 读已提交 > 可重复读 > 可串行化
(3)按拦截程度,从高到底
可串行化 > 可重复读 > 读已提交 > 读未提交
8、事务总结
(1)需要掌握的:
在代码里面会使用事务:
- conn.setAutoCommit(false);
- conn.commit();
- conn.rollback();
事务只是针对连接连接对象,如果再开一个连接对象,那么还是默认自动提交事务。
(2)需要了解的:
存在安全隐患
读的问题:
- 脏读:A事务读到了B事务未提交的数据。
- 不可重复读:A事务读到了B事务已提交的数据,造成前后两次查询结果不一致。
- 幻读:A事务读到了B事务insert的数据,造成前后查询结果不一致。
写的问题:
- 丢失更新:A事务更新数据,先提交,B事务也更新对应数据,后提交,造成A事务的提交被覆盖。
(3)隔离级别:
- 读未提交
引发了:脏读 - 读已提交
解决了:脏读
引发了:不可重复读 - 可重复读
解决了:脏读,不可重复读
未解决:幻读 - 可串行化
解决了:脏读,不可重复读,幻读
问题全部解决,效率却很低,事务排队执行
MySQL:默认的隔离级别是:可重复读。
Oracle:默认的隔离级别是:读已提交。
(4)丢失更新:
(5)解决丢失更新
- 悲观锁:可以在查询的时候,加入 for update。
- 乐观锁:要求程序员自己控制。
数据库连接池
数据库的连接对象创建工作,比较消耗性能。
可以一开始先在内存中开辟一块空间(集合),开始先往池子里面放置 多个连接对象。
后面需要连接的话,直接从池子里面去。不要去自己创建连接了。使用完毕,要记得归还连接。确保连接对象能循环利用。
1、自定义数据库连接池
代码实现:
public class MyDataSource implements DataSource {
// 默认初始化数据库连接池中有20个连接对象
private static List<Connection> list = new ArrayList<>(20);
// 单例模式,设置一个实例化对象
private static MyDataSource mds = new MyDataSource();
// 类加载时,初始化数据库连接池中的连接对象
static {
for (int i = 0; i < 20; i++) {
try {
Connection conn = DBUtil.getConnection();
mds.list.add(conn);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
System.out.println("静态代码块执行:" + mds.getList().size());
}
public static MyDataSource getMds() {
return mds;
}
public static List<Connection> getList() {
return list;
}
// 构造方法私有化,确保只有一个实例化对象
private MyDataSource() {
}
// 从连接池中取出一个数据库连接对象
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
if (list.size() > 1) {
Connection connRem = list.remove(0);
// 抛出包装类对象
conn = new ConnectionWrap(connRem, list);
} else { // 池子里的连接对象快用完了,再添加1个
list.add(list.get(0));
Connection connRem = list.remove(0);
conn = new ConnectionWrap(connRem, list);
}
return conn;
}
// 额外的方法,归还连接对象
public static void addBack(Connection conn) {
list.add(conn);
}
// 这里还有接口实现的其他方法,不用管它
}
出现的问题:
- 需要额外记住 addBack方法
- 单例。
- 无法面向接口编程。
怎么解决? 以addBack为切入点。
解决自定义数据库连接池出现的问题:
由于多了一个addBack()方法,所以使用这个连接池的地方,需要额外记住这个方法,并且还不能面向接口编程。
我们打算修改接口中的那个close方法。 原来的Connection对象的close方法,是真的关闭连接。
打算修改这个close方法,以后在调用close,并不是真的关闭,而是归还连接对象。
如何扩展某一个方法?
原有的方法逻辑,不是我们想要的,想修改成自己的逻辑。
- 直接改源码,可是无法实现。sun公司已经不允许修改源码了。
- 继承,必须得知道这个接口的具体实现是谁。也很难做到。
- 那就使用装饰者模式,额外添加某个功能。 (还有更高级的,动态代理,这里不讲)
2、DBCP开源连接池
先导入jar文件
(1)不使用配置文件的方式:
// dbcp不使用配置文件的方式
public class Test01 {
public static void main(String[] args) {
// 创建数据源对象
BasicDataSource dataSource = new BasicDataSource();
// 设置一系列配置信息
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/study_test");
dataSource.setUsername("root");
dataSource.setPassword("666666");
// 开始连接
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 获取连接对象
conn = dataSource.getConnection();
// 获取预编译的数据库连接对象
String sql = "select ename, job, sal from emp";
ps = conn.prepareStatement(sql);
// 执行sql语句
rs = ps.executeQuery();
// 处理查询结果集
while (rs.next()) {
String ename = rs.getString("ename");
String job = rs.getString("job");
double sal = rs.getDouble("sal");
System.out.println(ename + " " + job + " " + sal);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBClose.close(conn, ps, rs);
}
}
}
(2)使用配置文件的方式:
// dpcp使用配置文件的方式
public class Test02 {
public static void main(String[] args) {
/*// 创建数据源对象 BasicDataSource dataSource = new BasicDataSource(); // 绑定配置文件的信息 dataSource.setConnectionProperties("dbcpconfig.properties"); // 经过测试,此方法行不通*/
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 获取属性配置文件
InputStream in = new FileInputStream("src/dbcpconfig.properties");
Properties properties = new Properties();
properties.load(in);
// 绑定配置文件的信息
DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);
conn = dataSource.getConnection();
String sql = "select ename, job, sal from emp";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
String ename = rs.getString("ename");
String job = rs.getString("job");
double sal = rs.getDouble("sal");
System.out.println(ename + " " + job + " " + sal);
}
} catch (SQLException | FileNotFoundException throwables) {
throwables.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
DBClose.close(conn, ps, rs);
}
}
}
3、C3P0开源连接池
先导入jar文件到lib目录。
(1)不使用配置文件的方式
// c3p0不使用配置文件的方式
public class Test01 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 创建数据源对象
ComboPooledDataSource cpds = new ComboPooledDataSource();
// 设置信息
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/study_test");
cpds.setUser("root");
cpds.setPassword("666666");
// 获取连接
conn = cpds.getConnection();
// 获取预编译的数据库操作对象
String sql = "update t_user set pwd = ? where id = ?";
ps = conn.prepareStatement(sql);
// 给占位符传值
ps.setString(1, "123");
ps.setString(2, "1");
// 执行sql语句
int affectLine = ps.executeUpdate();
// 处理结果
System.out.println(affectLine == 1 ? "更新成功" : "更新失败");
} catch (SQLException | PropertyVetoException throwables) {
throwables.printStackTrace();
} finally {
DBClose.close(conn, ps, null);
}
}
}
(2)使用配置文件的方式
// c3p0使用配置文件的方式
public class Test02 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 创建默认的数据源对象
// 会自动绑定配置文件c3p0-config.xml,名字必须为这个,不能写错
ComboPooledDataSource cpds = new ComboPooledDataSource();
// 也可以指定连接oracle数据库
//ComboPooledDataSource cpds1 = new ComboPooledDataSource("oracle");
// 获取连接
conn = cpds.getConnection();
// 获取预编译的数据库操作对象
String sql = "update t_user set pwd = ? where id = ?";
ps = conn.prepareStatement(sql);
// 给占位符传值
ps.setString(1, "123");
ps.setString(2, "1");
// 执行sql语句
int affectLine = ps.executeUpdate();
// 处理结果
System.out.println(affectLine == 1 ? "更新成功" : "更新失败");
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBClose.close(conn, ps, null);
}
}
}
DBUtils
1、增删改
DBUtils只是帮我们简化了CRUD(增删改查)操作,连接数据库并不是它做的。
(1)针对增加,删除,更新操作
- queryRunner.update(sql, params);
使用update()方法,形式参数为语句sql,变长占位符的传值
测试代码:
public class Test01 {
public static void main(String[] args) {
try {
// 创建连接池对象
ComboPooledDataSource dataSource = new ComboPooledDataSource();
// 创建DBUtil操作对象,并绑定到连接池
QueryRunner queryRunner = new QueryRunner(dataSource);
// 执行sql语句,增删改,用update()方法
// 增
/*String sql = "insert into t_user values(null, ?, ?)"; int affectLine = queryRunner.update(sql, "wangwu", "abc123");*/
// 删
/*String sql = "delete from t_user where id = ?"; int affectLine = queryRunner.update(sql, 6);*/
// 改
String sql = "update t_user set pwd = ? where id = ?";
int affectLine = queryRunner.update(sql, "acb", 5);
// 处理更新结果
System.out.println(affectLine == 1 ? "更新成功" : "更新失败");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
2、查询
针对查询操作
- queryRunner.query(sql, rsh, params);
注意:接收类User的属性字段名字,必须对应数据库表中的列名,这样底层才能自动封装,否则值会为null。
测试代码:
public class Test02 {
public static void main(String[] args) {
// 创建数据库连接池对象
ComboPooledDataSource dataSource = new ComboPooledDataSource();
// 创建DbUtil操作对象,并绑定到连接池
QueryRunner queryRunner = new QueryRunner(dataSource);
try {
/*// 执行sql语句 String sql = "select id, username, pwd from t_user where id = ?"; // 返回一个用户的信息,手动封装到user中 User user = queryRunner.query(sql, new ResultSetHandler<User>() { @Override public User handle(ResultSet rs) throws SQLException { User u = new User(); while (rs.next()) { int id = rs.getInt("id"); String username = rs.getString("username"); String pwd = rs.getString("pwd"); u.setId(id); u.setUsername(username); u.setPwd(pwd); } return u; } }, 1); System.out.println(user);*/
// ------------------------------------------------------------------------
// 如果查询结果集是1行数据,使用BeanHandler更简单,不用手动去封装
/*String sql = "select * from t_user where id = ?"; User user = queryRunner.query(sql, new BeanHandler<User>(User.class), 1); System.out.println(user);*/
// 如果查询结果集是多行数据,使用BeanListHandler,返回一个list集合
// 底层:通过字节码获取实例对象
String sql = "select * from t_user where username like ?";
List<User> users = queryRunner.query(sql, new BeanListHandler<User>(User.class), "%a%");
for (User u : users) {
System.out.println(u);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
3、ResultSetHandler 常用的实现类
使用频率最高的两个:
- BeanHandler, 查询到的单个数据封装成一个对象
- BeanListHandler, 查询到的多个数据封装成一个List<对象>
以下其它的实现类也可能会用到:
- ArrayHandler,查询到的单行数据封装成一个数组。
- ArrayListHandler,查询到的多行数据封装成一个集合,集合里面的元素是数组。
- MapHandler,查询到的单个数据封装成一个map。
- MapListHandler,查询到的多个数据封装成一个集合,集合里面的元素是map。
- ColumnListHandler
- KeyedHandler
- ScalarHandler
总结
1、事务
使用命令行演示
使用代码演示
读的问题:脏读,不可重复读,幻读
写的问题:丢失更新
悲观锁
乐观锁
4个隔离级别:读未提交,读已提交,可重复读,可串行化。
2、开源数据库连接池
DBCP
不使用配置
使用配置.properties文件
C3P0
不使用配置
使用配置.xml文件,文件名固定(必须掌握)
自定义连接池
单例模式
装饰者模式
3、DBUtils
简化了我们的CRUD,里面定义了通用的CRUD方法。
queryRunner.update(sql, params);
queryRunner.query(sql, rsh, params);
还没有评论,来说两句吧...