🌊 使用 addBatch() / executeBatch() / clearBatch()
mysql服务器默认是关闭批处理的,我们需要通过一个参数,让mysql开启批处理的支持。
?rewriteBatchedStatements=true
写在配置文件的url后面
user=root password=123123 url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&rewriteBatchedStatements=true driverClass=com.mysql.jdbc.Driver
@Test public void testInsert1() throws Exception { Connection connection = JDBCUtils.getConnection(); String sql = "insert into goods(name) values(?);"; PreparedStatement ps = connection.prepareStatement(sql); long start = System.currentTimeMillis(); for (int i=0; i<20000; i++) { ps.setObject(1, "name_" + i); // ps.execute(); // 攒sql ps.addBatch(); if (i % 500 == 0) { // 执行Batch ps.executeBatch(); // 清空Batch ps.clearBatch(); } } long end = System.currentTimeMillis(); System.out.println(end - start); JDBCUtils.closeResource(connection, ps); }
更换jar包(新版支持批量插入)
🌊 修改事务提交的时机
@Test public void testInsert1() throws Exception { Connection connection = JDBCUtils.getConnection(); // 获取连接时取消事务的自动提交 connection.setAutoCommit(false); String sql = "insert into goods(name) values(?);"; PreparedStatement ps = connection.prepareStatement(sql); long start = System.currentTimeMillis(); for (int i=0; i<20000; i++) { ps.setObject(1, "name_" + i); // ps.execute(); // 攒sql ps.addBatch(); if (i % 500 == 0) { // 执行Batch ps.executeBatch(); // 清空Batch ps.clearBatch(); } } // 统一提交数据 connection.commit(); long end = System.currentTimeMillis(); System.out.println(end - start); JDBCUtils.closeResource(connection, ps); }
🥽 数据库事务
🌊 数据库事务介绍
- 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。
- 一组逻辑操作单元:一行或多行DML操作。
- 事务处理(事务操作)的原则:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。
- 为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态。
- 数据一旦提交,就不可回滚。
- 哪些操作会导致数据的自动提交?
- DDL操作一旦执行,都会自动提交。
set autocommit = false
的方式对DDL操作失效。 - DML操作默认情况下,一旦执行,就会自动提交。可以通过
set autocommit = false
的方式取消DML操作自动提交 - 默认在关闭连接时,会自动的提交数据。
🌊 考虑事务后的转账操作实现
public class TransactionTest { @Test public void test() { Connection connection = null; try { connection = JDBCUtils.getConnection(); // 取消事务的自动提交 connection.setAutoCommit(false); String sql1 = "update user_table set balance=balance-100 where user=?"; update(connection, sql2, "AA"); String sql2 = "update user_table set balance=balance+100 where user=?"; update(connection, sql1, "BB"); System.out.println("转账成功"); // 提交事务 connection.commit(); } catch (Exception e) { e.printStackTrace(); // 回滚事务 try { connection.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } finally { JDBCUtils.closeResource(connection, null); } } // 通用的增删改操作 public int update(Connection connection, String sql, Object ...args) { PreparedStatement ps = null; try { ps = connection.prepareStatement(sql); // 填充占位符 for (int i = 0; i < args.length; i++) { ps.setObject(i+1, args[i]); } // 执行sql return ps.executeUpdate(); } catch (Exception e) { e.fillInStackTrace(); } finally { // 修改为自动提交事务 // 恢复自动提交状态 try { connection.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } // 关闭资源 JDBCUtils.closeResource(null, ps); } return 0; } }
try { connection = JDBCUtils.getConnection(); // 取消事务的自动提交 connection.setAutoCommit(false); String sql1 = "update user_table set balance=balance-100 where user=?"; update(connection, sql1, "AA"); System.out.println(10 / 0); String sql2 = "update user_table set balance=balance+100 where user=?"; update(connection, sql2, "BB"); System.out.println("转账成功"); // 提交事务 connection.commit(); }
若此时 Connection 没有被关闭,还可能被重复使用,则需要恢复其自动提交状态
setAutoCommit(true)
。尤其是在使用数据库连接池技术时,执行close()方法前,建议恢复自动提交状态。
🌊 事务的ACID属性
- 原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
- 隔离性(Isolation):事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
🌊 数据库的并发问题
- 对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题:
- 脏读: 对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, T1读取的 内容就是临时且无效的。
- 不可重复读: 对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段并提交了操作。之后, T1再次读取同一个字段, 值就不同了。
- 幻读: 对于两个事务T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行。之后, 如果 T1 再次读取同一个表, 就会多出几行。
- 数据库事务的隔离性: 数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题。
🌊 四种隔离级别
- 一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱。
- 数据库提供的4种事务隔离级别:
- Oracle 支持的 2 种事务隔离级别:READ COMMITED, SERIALIZABLE。 Oracle 默认的事务隔离级别为: READ COMMITED。
- Mysql 支持 4 种事务隔离级别。Mysql 默认的事务隔离级别为: REPEATABLE READ。
🥽 DAO及相关实现类
🌊 BaseDAO
package DAO; import utils.JDBCUtils; import java.lang.reflect.Field; import java.sql.*; import java.util.ArrayList; import java.util.List; /** * 封装了针对于数据表的通用的操作 */ public class BaseDAO { /** * 通用的增删改操作 * @param connection 数据库连接对象 * @param sql sql语句 * @param args sql语句填充参数 * @return 返回本次增删改操作对数据库表影响的行数 */ public int update(Connection connection, String sql, Object ...args) { PreparedStatement ps = null; try { ps = connection.prepareStatement(sql); // 填充占位符 for (int i = 0; i < args.length; i++) { ps.setObject(i+1, args[i]); } // 执行sql return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭资源 JDBCUtils.closeResource(null, ps); } return 0; } /** * 通用的查询操作 * @param conn 数据库连接对象 * @param clazz 查询数据对应的类 * @param sql sql语句 * @param args sql语句填充参数 * @return 返回查询的结果 * @param <T> 泛型 */ public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object... args) { PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } rs = ps.executeQuery(); // 获取结果集的元数据 :ResultSetMetaData ResultSetMetaData rsmd = rs.getMetaData(); // 通过ResultSetMetaData获取结果集中的列数 int columnCount = rsmd.getColumnCount(); // 创建集合对象 ArrayList<T> list = new ArrayList<T>(); while (rs.next()) { T t = clazz.newInstance(); // 处理结果集一行数据中的每一个列:给t对象指定的属性赋值 for (int i = 0; i < columnCount; i++) { // 获取列值 Object columValue = rs.getObject(i + 1); // 获取每个列的列名 // String columnName = rsmd.getColumnName(i + 1); String columnLabel = rsmd.getColumnLabel(i + 1); // 给t对象指定的columnName属性,赋值为columValue:通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t, columValue); } list.add(t); } return list; } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(null, ps, rs); } return null; } /** * 用于查询特殊值的通用的方法 * @param conn 数据库连接对象 * @param sql sql语句 * @param args sql语句填充参数 * @return 返回查询的结果 * @param <E> 泛型 */ public <E> E getValue(Connection conn,String sql,Object...args){ PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for(int i = 0;i < args.length;i++){ ps.setObject(i + 1, args[i]); } rs = ps.executeQuery(); if(rs.next()){ return (E) rs.getObject(1); } } catch (SQLException e) { e.printStackTrace(); }finally{ JDBCUtils.closeResource(null, ps, rs); } return null; } }
🌊 BaseDAO(泛型+反射)
public abstract class BaseDAO<T> { private Class<T> clazz = null; // public BaseDAO(){ // // } { //获取当前BaseDAO的子类继承的父类中的泛型 Type genericSuperclass = this.getClass().getGenericSuperclass(); ParameterizedType paramType = (ParameterizedType) genericSuperclass; Type[] typeArguments = paramType.getActualTypeArguments();//获取了父类的泛型参数 clazz = (Class<T>) typeArguments[0];//泛型的第一个参数 } // 通用的增删改操作---version 2.0 (考虑上事务) public int update(Connection conn, String sql, Object... args) {// sql中占位符的个数与可变形参的长度相同! PreparedStatement ps = null; try { // 1.预编译sql语句,返回PreparedStatement的实例 ps = conn.prepareStatement(sql); // 2.填充占位符 for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]);// 小心参数声明错误!! } // 3.执行 return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } finally { // 4.资源的关闭 JDBCUtils.closeResource(null, ps); } return 0; } // 通用的查询操作,用于返回数据表中的一条记录(version 2.0:考虑上事务) public T getInstance(Connection conn, String sql, Object... args) { PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } rs = ps.executeQuery(); // 获取结果集的元数据 :ResultSetMetaData ResultSetMetaData rsmd = rs.getMetaData(); // 通过ResultSetMetaData获取结果集中的列数 int columnCount = rsmd.getColumnCount(); if (rs.next()) { T t = clazz.newInstance(); // 处理结果集一行数据中的每一个列 for (int i = 0; i < columnCount; i++) { // 获取列值 Object columValue = rs.getObject(i + 1); // 获取每个列的列名 // String columnName = rsmd.getColumnName(i + 1); String columnLabel = rsmd.getColumnLabel(i + 1); // 给t对象指定的columnName属性,赋值为columValue:通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t, columValue); } return t; } } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(null, ps, rs); } return null; } // 通用的查询操作,用于返回数据表中的多条记录构成的集合(version 2.0:考虑上事务) public List<T> getForList(Connection conn, String sql, Object... args) { PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } rs = ps.executeQuery(); // 获取结果集的元数据 :ResultSetMetaData ResultSetMetaData rsmd = rs.getMetaData(); // 通过ResultSetMetaData获取结果集中的列数 int columnCount = rsmd.getColumnCount(); // 创建集合对象 ArrayList<T> list = new ArrayList<T>(); while (rs.next()) { T t = clazz.newInstance(); // 处理结果集一行数据中的每一个列:给t对象指定的属性赋值 for (int i = 0; i < columnCount; i++) { // 获取列值 Object columValue = rs.getObject(i + 1); // 获取每个列的列名 // String columnName = rsmd.getColumnName(i + 1); String columnLabel = rsmd.getColumnLabel(i + 1); // 给t对象指定的columnName属性,赋值为columValue:通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t, columValue); } list.add(t); } return list; } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(null, ps, rs); } return null; } //用于查询特殊值的通用的方法 public <E> E getValue(Connection conn,String sql,Object...args){ PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for(int i = 0;i < args.length;i++){ ps.setObject(i + 1, args[i]); } rs = ps.executeQuery(); if(rs.next()){ return (E) rs.getObject(1); } } catch (SQLException e) { e.printStackTrace(); }finally{ JDBCUtils.closeResource(null, ps, rs); } return null; } }
🥽 数据库连接池
🌊 数据库连接池简介
- 在使用开发基于数据库的web程序时,传统的模式基本是按以下步骤:
- 在主程序(如servlet、beans)中建立数据库连接
- 进行sql操作
- 断开数据库连接
- 这种模式开发,存在的问题:
- 普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection加载到内存中,再验证用户名和密码(得花费0.05s~1s的时间)。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很好的重复利用。若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。
- 对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。(Java的内存泄漏,存在对象不能被回收)
- 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。
- 为解决传统开发中的数据库连接问题,可以采用数据库连接池技术。
- 数据库连接池的基本思想:就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。
- 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
- 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。
- 数据库连接池技术的优点
- 资源重用
由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。 - 更快的系统反应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间 - 新的资源分配手段
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源 - 统一的连接管理,避免数据库连接泄漏
在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露
- 多种开源的数据库连接池
- JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
- DBCP 是Apache提供的数据库连接池。tomcat 服务器自带dbcp数据库连接池。速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持。
- C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以。hibernate官方推荐使用
- Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
- BoneCP 是一个开源组织提供的数据库连接池,速度快
- Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池,但是速度不确定是否有BoneCP快
- DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池
- DataSource用来取代DriverManager来获取Connection,获取速度快,同时可以大幅度提高数据库访问速度。
- 数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。
- 当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但conn.close()并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。