JDBC高级经典剖析

    科技2023-11-29  75

    JDBC高级经典剖析

    1. 事务(Transaction)1.1. 事务概述1.2. 事务的四大特性(ACID)1.3. 数据库中的事务1.4. JDBC事务1.5. 事务的并发读问题1.5. 事务隔离级别1.6. 数据库的事务隔离级别1.7. JDBC设置隔离级别1.8. 事务总结 2 数据库连接池2.1. 数据库连接池的概念2.2. JDBC数据库连接池接口(DataSource)2.3. druid

    1. 事务(Transaction)

    1.1. 事务概述

    为了方便演示事务,我们需要创建一个account表:

    Drop table account; CREATE TABLE account( ID int(10), NAME VARCHAR(30), balance double(10,2) ); INSERT INTO account VALUES(1,'zs', 100000); INSERT INTO account VALUES(2,'ls', 100000); INSERT INTO account VALUES(3, 'ww', 100000); SELECT * FROM account;

    什么是事务?

    银行转账!张三转1000块到李四的账户,这其实需要两条SQL语句:

    给张三的账户减去1000元;给李四的账户加上1000元。

    如果在第一条SQL语句执行成功后,在执行第二条SQL语句之前,程序被中断了(可能是抛出了某个异常,也可能是其他什么原因),那么李四的账户没有加上10000元,而张三却减去了10000元。这肯定是不行的!

    你现在可能已经知道什么是事务了吧!事务中的多个操作,要么完全成功,要么完全失败!不可能存在成功一半的情况!也就是说给张三的账户减去10000元如果成功了,那么给李四的账户加上10000元的操作也必须是成功的;否则给张三减去10000元,以及给李四加上10000元都是失败的!

    1.2. 事务的四大特性(ACID)

    事务的四大特性是:面试!

    原子性(Atomicity): 事务中所有操作是不可再分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败。一致性(Consistency): 事务执行后,数据库状态与其它业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。隔离性(Isolation): 隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。持久性(Durability): 一旦事务提交成功,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。

    1.3. 数据库中的事务

    在默认情况下,每执行一条增、删、改SQL语句,都是一个单独的事务。如果需要在一个事务中包含多条SQL语句,那么需要开启事务和结束事务。

    结束事务:commit或rollback。

    在执行增、删、改一条SQL就开启了一个事务(事务的起点),然后可以去执行多条SQL语句,最后要结束事务,commit表示提交,即事务中的多条SQL语句所做出的影响会持久化到数据库中。或者rollback,表示回滚,即回滚到事务的起点,之前做的所有操作都被撤消了!

    下面演示zs给li转账10000元的示例:

    UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; ROLLBACK; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; COMMIT;

    1.4. JDBC事务

    Connection的三个方法与事务相关:

    setAutoCommit(boolean): 设置是否为自动提交事务,如果true(默认值就是true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务,如果设置false,那么就相当于开启了事务了;commit(): 提交结束事务;rollback(): 回滚结束事务。 public void transfer(boolean b) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手动提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where id=?"; pstmt = con.prepareStatement(sql); //操作 pstmt.setDouble(1, -10000); pstmt.setInt(2, 1); pstmt.executeUpdate(); // 在两个操作中抛出异常 if(b) { throw new Exception(); } pstmt.setDouble(1, 10000); pstmt.setInt(2, 2); pstmt.executeUpdate(); //提交事务 con.commit(); } catch(Exception e) { //回滚事务 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //关闭 JdbcUtils.close(con, pstmt); } }

    1.5. 事务的并发读问题

    五大并发事务问题: 因为并发事务导致的问题大致有5类,其中两类是更新问题,三类是读问题。

    脏读: 读取到另一个事务未提交数据;不可重复读: 两次读取不一致;幻读(虚读): 读到另一事务已提交数据。

    问题详解:

    脏读(dirty read): 读到未提交更新数据,共享资源是数据级别,修改数据才会发生。 时间事务A事务BT1开始事务T2开始事务T3查询账户余额为1000元T4取出500元把余额改为500元T5查看账户余额为500元(脏读)T6撤销事务,余额恢复为1000元T7汇入100元把余额改为600元T8提交事务

    A事务查询到了B事务未提交的更新数据,A事务依据这个查询结果继续执行相关操作。但是接着B事务撤销了所做的更新,这会导致A事务操作的是脏数据。(这是绝对不允许出现的事情)

    虚读(幻读)(phantom read): 读到已提交插入数据,共享资源是表级别,增删数据才会发生。 时间事务A事务BT1开始事务T2开始事务T3统计总存款数为10000元T4新增一个存款账户,存款为100元T5提交事务T6再次统计总存款数为10100元

    A事务第一次查询时,没有问题,第二次查询时查到了B事务已提交的新插入数据,这导致两次查询结果不同。(在实际开发中,很少会对相同数据进行两次查询,所以可以考虑是否允许虚读)

    不可重复读(unrepeatable read): 读到已提交更新数据 时间事务A事务BT1开始事务T2开始事务T3查询账户余额为1000元T4查询账户余额为1000元T5取出100元,把余额改为900元T6提交事务T7查询账户余额为900元(与T4读取的一不一致)

    不可重复读与虚读有些相似,都是两次查询的结果不同。后者是查询到了另一个事务已提交的新插入数据,而前者是查询到了另一个事务已提交的更新数据。

    1.5. 事务隔离级别

    哪种隔离级别最好?

    4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的。

    SERIALIZABLE(串行化) 当数据库系统使用SERIALIZABLE隔离级别时,一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。

    REPEATABLE READ(可重复读) 当数据库系统使用REPEATABLE READ隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。

    READ COMMITTED(读已提交数据) 当数据库系统使用READ COMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。

    READ UNCOMMITTED(读未提交数据) 当数据库系统使用READ UNCOMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。

    你可能会说,选择SERIALIZABLE,因为它最安全!没错,它是最安全,但它也是最慢的!而且也最容易产生死锁。四种隔离级别的安全性与性能成反比!最安全的性能最差,最不安全的性能最好!

    1.6. 数据库的事务隔离级别

    MySQL的默认隔离级别为REPEATABLE READ

    Oracle数据库支持READ COMMITTED 和 SERIALIZABLE这两种事务隔离级别。所以Oracle不支持脏读Oracle的默认隔离级别是READ COMMITTED

    查看mysql的默认隔离级别:select @@tx_isolation;

    1.7. JDBC设置隔离级别

    con. setTransactionIsolation(int level) 参数可选值如下:

    Connection.TRANSACTION_READ_UNCOMMITTED;Connection.TRANSACTION_READ_COMMITTED;Connection.TRANSACTION_REPEATABLE_READ;Connection.TRANSACTION_SERIALIZABLE。

    1.8. 事务总结

    事务的特性:ACID;事务开始边界与结束边界:开始边界(con.setAutoCommit(false)),结束边界(con.commit()或con.rollback());事务的隔离级别: READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。多个事务并发执行时才需要考虑并发事务。

    2 数据库连接池

    2.1. 数据库连接池的概念

    用池来管理Connection,这可以重复使用Connection。有了池,所以我们就不用自己来创建Connection,而是通过池来获取Connection对象。当使用完Connection后,调用Connection的close()方法也不会真的关闭Connection,而是把Connection“归还”给池。池就可以再利用这个Connection对象了。

    2.2. JDBC数据库连接池接口(DataSource)

    Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商可以让自己的连接池实现这个接口。这样应用程序可以方便的切换不同厂商的连接池!

    2.3. druid

    Druid首先是一个数据库连接池。Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。Druid是阿里巴巴开发的号称为监控而生的数据库连接池!

    数据源的配置:

    #Driver driverClassName=com.mysql.jdbc.Driver #数据库链接, url=jdbc:mysql://127.0.0.1:3306/tx_db #帐号 username=root #密码 password=root #检测数据库链接是否有效,必须配置 validationQuery=SELECT 'x' #初始连接数 initialSize=3 #最大连接池数量 maxActive=10 #配置0,当线程池数量不足,自动补充。 minIdle=0 #获取链接超时时间为1分钟,单位为毫秒。 maxWait=60000 #获取链接的时候,不校验是否可用,开启会有损性能。 testOnBorrow=false #归还链接到连接池的时候校验链接是否可用。 testOnReturn=false

    使用数据源的代码:

    public static void main(String[] args) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; String sql = "insert into person(p_id, pname, addr, birth) values( null, '志玲', '日本', '1997-5-9')"; try{ Properties properties = new Properties(); InputStream in = Test.class.getClassLoader().getResourceAsStream("druid.properties"); properties.load(in); DataSource dataSource = DruidDataSourceFactory.createDataSource(properties); // 获得连接: conn = dataSource.getConnection(); conn = dataSource.getConnection(); conn = dataSource.getConnection(); conn = dataSource.getConnection(); /*pstmt = conn.prepareStatement(sql); pstmt.executeUpdate();*/ }catch(Exception e){ e.printStackTrace(); }finally{ JDBCUtil.closeRes(rs); } }

    数据库连接池以后的应用是结合Spring来使用的,我们只需配置即可。

    Processed: 0.017, SQL: 8