事务 & 数据库连接池 & DBUtils

迷南。 2022-11-30 12:29 310阅读 0赞

事务 & 数据库连接池 & DBUtils

事务

Transaction,其实指的一组操作,里面包含许多个单一的逻辑。
只要有一个逻辑没有执行成功,那么都算失败。 所有的数据都回归到最初的状态(回滚)。

为什么要有事务?
为了确保逻辑的成功。例子:银行的转账。


1、使用命令行方式演示事务。

(1)开启事务命令:start transaction;

(2)提交,回滚事务命令:
commit:提交事务,数据将会写到磁盘上的数据库
rollback:数据回滚,回到最初的状态。

(3)关闭自动提交功能
在这里插入图片描述

(4)演示事务
在这里插入图片描述


2、使用代码方式演示事务

代码里面的事务,主要是针对连接来的。

(1)通过conn.setAutoCommit(false) 来关闭自动提交的设置
(2)提交事务:conn.commit();
(3)回滚事务:conn.rollback();

  1. public static void main(String[] args){
  2. Connection conn = null;
  3. PreparedStatement ps = null;
  4. ResultSet rs = null;
  5. try {
  6. conn = JDBCUtil.getConn();
  7. //连接,事务默认就是自动提交的。 关闭自动提交。
  8. conn.setAutoCommit(false);
  9. String sql = "update account set money = money + ? where id = ?";
  10. ps = conn.prepareStatement(sql);
  11. //扣钱,扣ID为1 的100块钱
  12. ps.setInt(1, -100);
  13. ps.setInt(2, 1);
  14. ps.executeUpdate();
  15. // 模拟中间出现异常
  16. int a = 10 / 0 ;
  17. //加钱,给ID为2 加100块钱
  18. ps.setInt(1, 100);
  19. ps.setInt(2, 2);
  20. ps.executeUpdate();
  21. //成功: 提交事务。
  22. conn.commit();
  23. } catch (SQLException e) {
  24. try {
  25. //事变: 回滚事务
  26. conn.rollback();
  27. } catch (SQLException e1) {
  28. e1.printStackTrace();
  29. }
  30. e.printStackTrace();
  31. } finally {
  32. JDBCUtil.release(conn, ps, rs);
  33. }
  34. }

3、事务的特性

(1)原子性
事务中包含的逻辑,不可分割。

(2)一致性
事务执行前后,数据完整性。
语句全部执行成功,或者全部失败。

(3)隔离性
事务与事务之间没有影响,相互隔离。

(4)持久性
事务执行成功,那么数据应该持久保存到磁盘上。


4、事务的安全隐患

不考虑隔离级别设置,那么会出现以下问题。

读的问题:脏读,不可重读读,幻读。

  • 脏读:A事务读到了B事务还未提交的数据。
  • 不可重复读:A事务读到了B事务提交的数据,造成了前后两次查询结果不一致。

5、读未提交 演示

(1)设置A窗口的隔离级别为:读未提交
\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z1kaPc00-1598238423897)(img/img03.png)\]

(2)AB两个窗口都分别开启事务
在这里插入图片描述


6、读已提交 演示

(1)设置A窗口的隔离级别为:读已提交
\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SyNoW2C1-1598238423903)(img/img05.png)\]

(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、自定义数据库连接池

代码实现:

  1. public class MyDataSource implements DataSource {
  2. // 默认初始化数据库连接池中有20个连接对象
  3. private static List<Connection> list = new ArrayList<>(20);
  4. // 单例模式,设置一个实例化对象
  5. private static MyDataSource mds = new MyDataSource();
  6. // 类加载时,初始化数据库连接池中的连接对象
  7. static {
  8. for (int i = 0; i < 20; i++) {
  9. try {
  10. Connection conn = DBUtil.getConnection();
  11. mds.list.add(conn);
  12. } catch (SQLException throwables) {
  13. throwables.printStackTrace();
  14. }
  15. }
  16. System.out.println("静态代码块执行:" + mds.getList().size());
  17. }
  18. public static MyDataSource getMds() {
  19. return mds;
  20. }
  21. public static List<Connection> getList() {
  22. return list;
  23. }
  24. // 构造方法私有化,确保只有一个实例化对象
  25. private MyDataSource() {
  26. }
  27. // 从连接池中取出一个数据库连接对象
  28. @Override
  29. public Connection getConnection() throws SQLException {
  30. Connection conn = null;
  31. if (list.size() > 1) {
  32. Connection connRem = list.remove(0);
  33. // 抛出包装类对象
  34. conn = new ConnectionWrap(connRem, list);
  35. } else { // 池子里的连接对象快用完了,再添加1个
  36. list.add(list.get(0));
  37. Connection connRem = list.remove(0);
  38. conn = new ConnectionWrap(connRem, list);
  39. }
  40. return conn;
  41. }
  42. // 额外的方法,归还连接对象
  43. public static void addBack(Connection conn) {
  44. list.add(conn);
  45. }
  46. // 这里还有接口实现的其他方法,不用管它
  47. }

出现的问题:

  • 需要额外记住 addBack方法
  • 单例。
  • 无法面向接口编程。

怎么解决? 以addBack为切入点。


解决自定义数据库连接池出现的问题:

由于多了一个addBack()方法,所以使用这个连接池的地方,需要额外记住这个方法,并且还不能面向接口编程。

我们打算修改接口中的那个close方法。 原来的Connection对象的close方法,是真的关闭连接。

打算修改这个close方法,以后在调用close,并不是真的关闭,而是归还连接对象。


如何扩展某一个方法?

原有的方法逻辑,不是我们想要的,想修改成自己的逻辑。

  • 直接改源码,可是无法实现。sun公司已经不允许修改源码了。
  • 继承,必须得知道这个接口的具体实现是谁。也很难做到。
  • 那就使用装饰者模式,额外添加某个功能。 (还有更高级的,动态代理,这里不讲)

2、DBCP开源连接池

先导入jar文件

(1)不使用配置文件的方式:

  1. // dbcp不使用配置文件的方式
  2. public class Test01 {
  3. public static void main(String[] args) {
  4. // 创建数据源对象
  5. BasicDataSource dataSource = new BasicDataSource();
  6. // 设置一系列配置信息
  7. dataSource.setDriverClassName("com.mysql.jdbc.Driver");
  8. dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/study_test");
  9. dataSource.setUsername("root");
  10. dataSource.setPassword("666666");
  11. // 开始连接
  12. Connection conn = null;
  13. PreparedStatement ps = null;
  14. ResultSet rs = null;
  15. try {
  16. // 获取连接对象
  17. conn = dataSource.getConnection();
  18. // 获取预编译的数据库连接对象
  19. String sql = "select ename, job, sal from emp";
  20. ps = conn.prepareStatement(sql);
  21. // 执行sql语句
  22. rs = ps.executeQuery();
  23. // 处理查询结果集
  24. while (rs.next()) {
  25. String ename = rs.getString("ename");
  26. String job = rs.getString("job");
  27. double sal = rs.getDouble("sal");
  28. System.out.println(ename + " " + job + " " + sal);
  29. }
  30. } catch (SQLException throwables) {
  31. throwables.printStackTrace();
  32. } finally {
  33. DBClose.close(conn, ps, rs);
  34. }
  35. }
  36. }

(2)使用配置文件的方式:

  1. // dpcp使用配置文件的方式
  2. public class Test02 {
  3. public static void main(String[] args) {
  4. /*// 创建数据源对象 BasicDataSource dataSource = new BasicDataSource(); // 绑定配置文件的信息 dataSource.setConnectionProperties("dbcpconfig.properties"); // 经过测试,此方法行不通*/
  5. Connection conn = null;
  6. PreparedStatement ps = null;
  7. ResultSet rs = null;
  8. try {
  9. // 获取属性配置文件
  10. InputStream in = new FileInputStream("src/dbcpconfig.properties");
  11. Properties properties = new Properties();
  12. properties.load(in);
  13. // 绑定配置文件的信息
  14. DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);
  15. conn = dataSource.getConnection();
  16. String sql = "select ename, job, sal from emp";
  17. ps = conn.prepareStatement(sql);
  18. rs = ps.executeQuery();
  19. while (rs.next()) {
  20. String ename = rs.getString("ename");
  21. String job = rs.getString("job");
  22. double sal = rs.getDouble("sal");
  23. System.out.println(ename + " " + job + " " + sal);
  24. }
  25. } catch (SQLException | FileNotFoundException throwables) {
  26. throwables.printStackTrace();
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. } finally {
  30. // 释放资源
  31. DBClose.close(conn, ps, rs);
  32. }
  33. }
  34. }

3、C3P0开源连接池

先导入jar文件到lib目录。

(1)不使用配置文件的方式

  1. // c3p0不使用配置文件的方式
  2. public class Test01 {
  3. public static void main(String[] args) {
  4. Connection conn = null;
  5. PreparedStatement ps = null;
  6. try {
  7. // 创建数据源对象
  8. ComboPooledDataSource cpds = new ComboPooledDataSource();
  9. // 设置信息
  10. cpds.setDriverClass("com.mysql.jdbc.Driver");
  11. cpds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/study_test");
  12. cpds.setUser("root");
  13. cpds.setPassword("666666");
  14. // 获取连接
  15. conn = cpds.getConnection();
  16. // 获取预编译的数据库操作对象
  17. String sql = "update t_user set pwd = ? where id = ?";
  18. ps = conn.prepareStatement(sql);
  19. // 给占位符传值
  20. ps.setString(1, "123");
  21. ps.setString(2, "1");
  22. // 执行sql语句
  23. int affectLine = ps.executeUpdate();
  24. // 处理结果
  25. System.out.println(affectLine == 1 ? "更新成功" : "更新失败");
  26. } catch (SQLException | PropertyVetoException throwables) {
  27. throwables.printStackTrace();
  28. } finally {
  29. DBClose.close(conn, ps, null);
  30. }
  31. }
  32. }

(2)使用配置文件的方式

  1. // c3p0使用配置文件的方式
  2. public class Test02 {
  3. public static void main(String[] args) {
  4. Connection conn = null;
  5. PreparedStatement ps = null;
  6. try {
  7. // 创建默认的数据源对象
  8. // 会自动绑定配置文件c3p0-config.xml,名字必须为这个,不能写错
  9. ComboPooledDataSource cpds = new ComboPooledDataSource();
  10. // 也可以指定连接oracle数据库
  11. //ComboPooledDataSource cpds1 = new ComboPooledDataSource("oracle");
  12. // 获取连接
  13. conn = cpds.getConnection();
  14. // 获取预编译的数据库操作对象
  15. String sql = "update t_user set pwd = ? where id = ?";
  16. ps = conn.prepareStatement(sql);
  17. // 给占位符传值
  18. ps.setString(1, "123");
  19. ps.setString(2, "1");
  20. // 执行sql语句
  21. int affectLine = ps.executeUpdate();
  22. // 处理结果
  23. System.out.println(affectLine == 1 ? "更新成功" : "更新失败");
  24. } catch (SQLException throwables) {
  25. throwables.printStackTrace();
  26. } finally {
  27. DBClose.close(conn, ps, null);
  28. }
  29. }
  30. }

DBUtils

1、增删改

DBUtils只是帮我们简化了CRUD(增删改查)操作,连接数据库并不是它做的。

(1)针对增加,删除,更新操作

  • queryRunner.update(sql, params);
    使用update()方法,形式参数为语句sql,变长占位符的传值

测试代码:

  1. public class Test01 {
  2. public static void main(String[] args) {
  3. try {
  4. // 创建连接池对象
  5. ComboPooledDataSource dataSource = new ComboPooledDataSource();
  6. // 创建DBUtil操作对象,并绑定到连接池
  7. QueryRunner queryRunner = new QueryRunner(dataSource);
  8. // 执行sql语句,增删改,用update()方法
  9. // 增
  10. /*String sql = "insert into t_user values(null, ?, ?)"; int affectLine = queryRunner.update(sql, "wangwu", "abc123");*/
  11. // 删
  12. /*String sql = "delete from t_user where id = ?"; int affectLine = queryRunner.update(sql, 6);*/
  13. // 改
  14. String sql = "update t_user set pwd = ? where id = ?";
  15. int affectLine = queryRunner.update(sql, "acb", 5);
  16. // 处理更新结果
  17. System.out.println(affectLine == 1 ? "更新成功" : "更新失败");
  18. } catch (SQLException throwables) {
  19. throwables.printStackTrace();
  20. }
  21. }
  22. }

2、查询

针对查询操作

  • queryRunner.query(sql, rsh, params);

注意:接收类User的属性字段名字,必须对应数据库表中的列名,这样底层才能自动封装,否则值会为null。

测试代码:

  1. public class Test02 {
  2. public static void main(String[] args) {
  3. // 创建数据库连接池对象
  4. ComboPooledDataSource dataSource = new ComboPooledDataSource();
  5. // 创建DbUtil操作对象,并绑定到连接池
  6. QueryRunner queryRunner = new QueryRunner(dataSource);
  7. try {
  8. /*// 执行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);*/
  9. // ------------------------------------------------------------------------
  10. // 如果查询结果集是1行数据,使用BeanHandler更简单,不用手动去封装
  11. /*String sql = "select * from t_user where id = ?"; User user = queryRunner.query(sql, new BeanHandler<User>(User.class), 1); System.out.println(user);*/
  12. // 如果查询结果集是多行数据,使用BeanListHandler,返回一个list集合
  13. // 底层:通过字节码获取实例对象
  14. String sql = "select * from t_user where username like ?";
  15. List<User> users = queryRunner.query(sql, new BeanListHandler<User>(User.class), "%a%");
  16. for (User u : users) {
  17. System.out.println(u);
  18. }
  19. } catch (SQLException throwables) {
  20. throwables.printStackTrace();
  21. }
  22. }
  23. }

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);


发表评论

表情:
评论列表 (有 0 条评论,310人围观)

还没有评论,来说两句吧...

相关阅读