10.09JavaWeb之JDBC后程

10.09JavaWeb之JDBC后程

后程事务

  • 数据库事务

  • DAO及其实现类

  • 数据库连接池

  • Apache-DButils实现CRUD操作

    • QueryRunner类的使用


数据库事务

try...catch...throws使用的选择:

  • 单独的方法出现的异常使用throws

  • 当多个方法组合使用的时候使用try...catch...,当方法一抛出异常的时候捕获异常执行方法二

经典模拟场景--->转账

AA给BB转账--->数据库事务

    /*
   1、针对于数据表账户表
   2、用户AA给用户BB转账100
   3、sql语句
       1、update user_table set balance = balance - 100 where user = 'AA';
       2、update user_table set balance = balance + 100 where user = 'BB';
    */
   @Test
   public void testUpdate() {
       String sql1 = "update user_table set balance = balance - 100 where user = ?;";
       update(sql1, "AA");

       String sql2 = "update user_table set balance = balance + 100 where user = ?;";
       update(sql2, "BB");

       System.out.println("操作成功!");
  }

如果发生了异常,那么就会导致数据的最终不一致

    /*
   1、针对于数据表账户表
   2、用户AA给用户BB转账100
   3、sql语句
       1、update user_table set balance = balance - 100 where user = 'AA';
       2、update user_table set balance = balance + 100 where user = 'BB';
    */
   @Test
   public void testUpdate() {
       String sql1 = "update user_table set balance = balance - 100 where user = ?;";
       update(sql1, "AA");

       //模拟异常
       System.out.println(10 / 0);

       String sql2 = "update user_table set balance = balance + 100 where user = ?;";
       update(sql2, "BB");

       System.out.println("操作成功!");
  }
/*
如果出现这样的操作没有使用事务进行数据操作,就会导致AA少了100,BB没有增加100
*/

事务的概念:

  • 一组逻辑操作单元,使数据从一种状态变换到另一种状态--->称为一个数据库事务

事务处理(事务操作)的特点:

  • 一个事务执行多个操作时,要么所有的事务都被提交(Commit)

  • 有一个事务提交失败整个事务就进行回滚(Rollback)

确保数据的一致性的特点:

  • 数据的操纵应当是离散的成组的逻辑单元

  • 整个单元当中有一部分失败应当视为事务的失败,数据全部回滚

/*
什么是数据库事务?
1、一组逻辑操作单元:一个或多个DML操作
   1、刚才的AA给BB转账:涉及两个DML操作。是一个事务
   2、AA给BB转账,CC给DD转账这是两个事务
   3、AA给BB转账,CC给BB转账这也是两个事务。
2、事务处理的原则:
   1、事务作为一个工作单元执行,出现了故障也不能改变这种执行方式--->整体提交or事务回滚
事务回滚机制:
1、事务回滚只回滚到最近的commit操作--->数据一旦提交就不可回滚
2、需要关注哪些操作会导致数据的自动提交
   1、DDL操作一旦执行都会自动提交--->set autocommit = false对于DDL操作失效(DDL操作--->创建一个表、修改一个表中的字段、删除一张表独立的就相当于是一个事务(DDL操作)
   2、DML(增删改查)默认的情况下,一旦执行,自动提交--->可以通过set autocommit = false的方式取消DML操作的自动提交
   3、关闭连接的时候也会将没有提交的数据进行自动提交--->关闭连接会进行一次commit--->在执行(增删改查)的操作的时候不需要关闭数据库连接。保证多个DML操作作为一个事务进行提交
       1、获取链接
       2、进行第一个DML操作
       3、不关闭连接进行第二个DML操作
       调用方法的时候外部传入一个数据库连接
   防止事务的自动提交就要避免上诉的三件事
*/

示例代码:

统一的驱动连接调用

    //外部传入连接
   //通用的增删改查方法--->version 1.0
   public int update(Connection conn,String sql, Object ...args) {
       PreparedStatement ps = null;
       try {
           //获取链接
           conn = JDBCUtils.getConnection();
           //预编译sql
           ps = conn.prepareStatement(sql);
           //填充占位符--->使用流的形式
           for (int i = 0; i < args.length; i++) {
               ps.setObject(i + 1, args[i]);
          }
           //执行语句
           return ps.executeUpdate();
      }catch (Exception e) {
           e.printStackTrace();
      }finally {
           //关闭资源
           /*
           1、由于是外部传入的连接,所以不需要关闭Connection连接
            */
           JDBCUtils.closeResource(null, ps);
      }
       return 0;
  }

操作过程:

    //#########考虑数据库事务后的转账操作##########
   @Test
   public void testUpdateWithTransaction() {
       Connection conn = null;
       try {
           //获取连接
           conn = JDBCUtils.getConnection();
           System.out.println(conn.getAutoCommit());
           //设置取消事务的自动提交
           conn.setAutoCommit(false);
           System.out.println(conn.getAutoCommit());

           String sql1 = "update user_table set balance = balance - 100 where user = ?;";
           update(conn, sql1, "AA");

           //模拟异常

           String sql2 = "update user_table set balance = balance + 100 where user = ?;";
           update(conn, sql2, "BB");

           System.out.println("操作成功!");

           //提交事务
           conn.commit();
      }catch (Exception e) {
           e.printStackTrace();
           //异常之后回滚事务
           try {
               conn.rollback();
          }catch (SQLException e1) {
               System.out.println("事务回滚异常!");
               e1.printStackTrace();
          }
      }finally {
           //将自动提交改成默认值
           try {
               conn.setAutoCommit(true);
          }catch (SQLException e2) {
               e2.printStackTrace();
          }
           //统一关闭资源
           JDBCUtils.closeResource(conn, null);
      }
  }

注意:

  • 如果Connection没有被关闭,需要恢复自动提交状态。使用setAutoCommit(true)方法。

  • 使用数据库连接池的时候执行close()方法前需恢复自动提交状态

数据库事务的ACID属性

  • 原子性(Atomicity)

  • 一致性(Consistency)

  • 隔离性(Isolation)

  • 持久性(Durability)


原子性

概念:

事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生

一致性

概念:

事务必须使数据库从一个一致性状态变换到另一个一致性状态

隔离性

概念:

一个事务的执行不能被其他事务干扰。

  • 一个事务内部的操作及使用的数据并对并发的其他事务是隔离的

  • 并发执行的各个事务之间不能互相干扰

特点:

  • 数据库系统必须具有隔离并发运行各个事务的能力,不会互相影响。避免各种并发问题

  • 隔离级别越高,数据一致性越好,并发性越弱

持久性

数据库的并发问题:

发生场景:

同时运行多个事务,事务访问数据库的相同数据。没有采取隔离机制就会导致并发问题。

并发问题分类:

  • 脏读:两个事务T1和T2。T1读取已经被T2更新但还没有被提交的字段。此时T2进行回滚操作。T1读取到的数据就是临时且无效的。

  • 不可重复读:两个事务T1和T2。T1读取了一个字段,然后T2更新了该字段,T1再次读取同一个字段,值就不同了--->针对update操作

  • 幻读:两个事务T1和T2。T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行,如果T1再次读同一个表,会多出几行。--->针对insert操作


四种隔离级别
隔离级别描述
Read Uncommitted 允许事务读取未被其他事务提交的变更。脏读、不可重复读、幻读问题都会出现
Read Commited 只允许事务读取已经被其他事务提交的变更。可避免脏读但不可重复读和幻读问题仍可能出现
Repeatable Read(可重复读) 确保事务可以多次从一个字段中读取相同的值。在该事务持续期间禁止其他事物对该字段进行更新。避免脏读、不可重复读但幻读仍然存在
Serializable(串行化) 确保事务可以从一个表中读取相同的行,事务持续期间禁止其他事物对该表执行插入、更新、删除操作所。所有并发问题都可以避免,但性能十分低下

MySql默认的事务隔离级别:

Repeatable Read--->并发性较差,一致性较好

持久性

概念:

一个事务一旦被提交,对数据库中数据的改变是永久性的。接下来的其他操作和数据库故障不会对其有任何影响

MySql中设置隔离级别

启动mysql的特点:

  • 每启动一个mysql程序就会获得一个单独的数据库连接

  • 每个数据库连接都有一个全局变量@@tx_isolation表示当前的事务隔离级别

查看当前的隔离级别:

SELECT @@tx_isolation;

设置当前mysql链接的隔离级别:

set transaction isolation level read committed;
/*
set + 事务单词(transaction) + 隔离级别单词(isolation level) + 要设置成的隔离级别单词(read committed)
*/

设置数据库系统的全局的隔离级别:

set global transaction isolation level read committed;
/*
原有的语法上在事务前面加上adj.全面的
此时需要退出客户端在重新进入客户端
*/

创建数据库用户:

create user xxx identified by aaa;
#设置用户名:xxx,密码:aaa
#这时候只会默认提供一个数据库的权限给到该用户--->information_schema

授予权限:

#授权通过网络方式登录xxx账户,有所有库所有表的全部权限,设置密码
grant all privileges on *.* to xxx@'%' identified by '密码';

#给xxx用户使用本地命令行方式,授予test这个库下的所有表的增删改查权限
grant select,insert,delete,update on xxx.* to xxx@localhost identified by '密码'
/*
grant关键字 + 要赋予的权限 + 数据库.表名(*代表全部表) + to(表示给到哪个用户) + 用户名 + identified by + 密码
*/

先设置事务不自动提交:

set autocommit = false;

在进行查询(此时的事务并没有自动提交):

#使用root用户进行查询--->已经设置autocommit = false;
select * from user_table where `user` = 'cc';
#在使用junkingboy用户进行修改--->已经设置autocommit = false;其目的是为了测试mysql的默认事务隔离级别
update user_table set `balance` = 3000 where `user` = 'cc';
#此时事务未曾提交。root用户查出来的结果还是2000,junkingboy用户可以查出结果为3000。
#因为mysql默认的隔离级别是可重复读。所以即便junkingboy提交了事务但是root没有提交事务root用户查询的结果依旧不会是3000而是2000.
#使用root用户提交事务,这样就可以查询出junkingboy用户修改以后的数据

修改全局事务的隔离级别:

set global transaction isolation level read committed;
#重新进入两个用户的客户端
#设置不自动提交事务
#在junkingboy用户中提交update事务,然后再root用户中再进行读取
#root用户未提交事务,junkingboy用户提交了事务。那么再root得事务当中可以读取到junkingboy提交以后得数据。
#没有解决不可重复读得问题

再次修改全局事务隔离级别:

set global transaction isolation level read uncommitted;
#重新进入客户端
#设置不自动提交事务
#在junkingboy用户中提交update事务,然后再root用户中再进行读取
#root用户未提交事务,junkingboy用户也未提交了事务。那么再root得事务当中可以读取到junkingboy提交以后得数据。
#没有解决脏读的问题

Java代码层面设置事务隔离级别:--->该设置的隔离级别是针对连接的全局隔离级别

统一的查询方法:

/*
1、将其设置为事务的处理方式。--->将事务作为参数传入函数当中
注意:
1、不要再次创建链接Connection
2、关闭的时候不要关闭连接
通用查询操作,用于返回数据表中的一条数据(考虑事务操作)
*/
public <T> T getInstance(Connection conn, Class<T> clazz, String sql, Object ...args) {
// Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// //获取数据库连接
// conn = JDBCUtils.getConnection();
//预编译sql
ps = conn.prepareStatement(sql);
//填充占位符
for (int i=0; i<args.length; i++) {
ps.setObject(i+1, args[i]);
}
//执行sql保存为结果集对象
rs = ps.executeQuery();

//获取结果集元数据
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数
int columnCount = rsmd.getColumnCount();
//获取结果集
if (rs.next()) {
//通过反射获取运行时加载类建立对象的引用--->反射+泛型
T t = clazz.newInstance(); //--->任何一个类在提供一个JavaBean对象的时候要提供一个空参的public权限的构造器,在这里使用
/*
方法当中返回一个t
t由当前类决定的
*/
//动态的获取列,列的数量为列数
for (int j=0; j<columnCount; j++) {
//动态的获取列值--->结果集当中获取列值
Object columnValue = rs.getObject(j+1);
//获取每列的列名
String columnLabel = rsmd.getColumnLabel(j+1);
//动态获取加载的类的属性--->获取到域(T类型的)
Field field = clazz.getField(columnLabel);
//设置私有属性可访问
field.setAccessible(true);
//将对象属性设置成列值
field.set(t, columnValue);
}
return t;
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//关闭资源--->注意不要关闭连接
JDBCUtils.closeResource(null, ps, rs);
}
return null;
}

查询操作:

    //针对同一张表的同一个数据进行操作。演示事务隔离级别
@Test
public void testTransactionSelect() throws Exception {
Connection conn = JDBCUtils.getConnection();

//查看数据库隔离级别
System.out.println(conn.getTransactionIsolation());

//设置数据库的隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

//取消自动提交事务
conn.setAutoCommit(false);

//调用查询方法。因为返回的是一个对象。所以需要新建一个JavaBean的类--->返回的是一个User类对象
//sql语句
String sql = "select user, password, balance from user_table where user = ?;";
//注意填充占位符
User user = getInstance(conn, User.class, sql, "cc");

System.out.println(user);
}

修改操作:

    @Test
public void testTransactionUpdate() throws Exception {
//获取连接
Connection conn = JDBCUtils.getConnection();

//调用update方法
TransactionTest tt = new TransactionTest();

//取消自动提交事务
conn.setAutoCommit(false);

//sql
String sql = "update user_table set balance = ? where user = ?;";
tt.update(conn, sql, 5000,"CC");

//线程等待15s
Thread.sleep(15000);
System.out.println("修改结束!");
}

理解脏读、不可重复读、幻读

脏读

脏读是指:

两个事务A和B,同时设置不自动提交。A读取表中字段1,未提交事务,同时B修改表中字段1,未提交事务。此时A再去查询字段1(还在当前事务内)查询到的结果是B修改后的结果。

幻读

幻读是指:

两个事务A和B,同时设置不自动提交。A读取表中字段1,未提交事务,同时B修改表中字段1并且提交事务。此时A再去查询字段1(还在当前事务内)查询到的结果是B修改后的结果。

不可重复读

不可重复读是指:

两个事务A和B,同时设置不自动提交。A读取表中字段1,未提交事务,同时B修改表中字段1未曾提交事务。此时A再去查询字段1(还在当前事务内)查询到的结果是未修改的结果。此时B提交事务,A再去查询字段1(还在当前事务内)查询到的结果还是未修改的结果。

只有当A也提交了事务并且再次查询才能够查询到已修改的数据

posted @ 2021-10-12 19:11  俊king  阅读(69)  评论(0编辑  收藏  举报