连接数据库

连接方式1

  1. 直接获取Driver对象
  2. 写入连接信息
  3. 使用connect方法连接数据库
方法 说明
connect(String url, Properties info) 尝试使数据库连接到给定的URL
  • url:要连接数据库的URL
  • info:任意字符串标签/值对列表作为连接参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConnectionTest {
@Test
public void testConnection() throws SQLException {
// 获取Driver实现类对象
Driver driver = new com.mysql.jdbc.Driver();

String url = "jdbc:mysql://localhost:3306/test";
Properties info = new Properties();
// 设置连接的用户名和名称,user和password是固定的写法
info.setProperty("user","root");
info.setProperty("password","root");

Connection connect = driver.connect(url, info);

System.out.println(connect);
}
}

连接方式2

  1. 使用反射获取Driver对象
  2. 写入连接信息
  3. 使用connect方法连接数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConnectionTest {
@Test
public void testConnection() throws Exception {
// 使用反射获取Driver实现类对象
Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();

// 提供需要连接的数据库
String url = "jdbc:mysql://localhost:3306/test";
// 提供连接需要的用户和密码
Properties info = new Properties();
info.setProperty("user","root");
info.setProperty("password","root");

// 获取链接
Connection connect = driver.connect(url, info);

System.out.println(connect);

}
}

连接方式3

  1. 使用反射获取Driver对象
  2. 设置连接信息
  3. 注册数据库驱动
  4. 获取连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ConnectionTest {
@Test
public void testConnection() throws Exception {
// 使用反射获取Driver实现类对象
Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();

// 提供需要连接的数据库
String url = "jdbc:mysql://localhost:3306/test";
// 提供连接需要的用户和密码
String user = "root";
String password = "root";

// 注册驱动
DriverManager.registerDriver(driver);

// 获取连接
Connection connection = DriverManager.getConnection(url, user, password);

System.out.println(connection);
}
}
  • 使用DriverManager(驱动管理器)替换Driver,DriverManager是jdk提供的一个类,用来完成获取连接的操作

连接方式4

  1. 加载连接信息
  2. 使用反射加载数据驱动
  3. 获取连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConnectionTest {
@Test
public void testConnection() throws Exception {
// 提供连接所需信息
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";

// 使用反射加载驱动
Class.forName("com.mysql.jdbc.Driver");

// 获取连接
Connection connection = DriverManager.getConnection(url, user, password);

System.out.println(connection);
}
}

  • 与方式3相比,方式4省略了如下代码
1
2
Driver driver = (Driver) clazz.newInstance();
DriverManager.registerDriver(driver);
  • 理由是,在com.mysql.jdbc.Driver类中有如下所示的静态代码块,使用Class.forName()加载这个驱动时就会执行这个静态代码块,实现了注册驱动
1
2
3
4
5
6
7
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}

连接方式5(推荐

  1. 使用流API加载properties文件信息(类—>类加载器—>ResourceAsStream)
  2. 使用反射加载驱动
  3. 获取连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ConnectionTest {
@Test
public void testConnection() throws Exception {
// 获取连接所需信息
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");

Properties info = new Properties();
info.load(is);
String url = info.getProperty("url");
String user = info.getProperty("user");
String password = info.getProperty("password");
String driverClass = info.getProperty("driverClass");

// 使用反射加载驱动
Class.forName(driverClass);

// 获取连接
Connection connection = DriverManager.getConnection(url, user, password);

System.out.println(connection);
}
}
  • 其中配置文件jdbc.properties在src目录下
1
2
3
4
url=jdbc:mysql://localhost:3306/test
user=root
password=root;
driverClass=com.mysql.jdbc.Driver
  • 使用配置文件的好处
    1. 实现代码和数据的分离,如果需要修改配置信息,直接在配置文件中进行修改,不需要深入代码
    2. 如果修改了配置信息,省去了重新编译的过程

PreparedStatement

JDBCUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class JDBCUtils {
public static Connection getConnection() throws Exception {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");

Properties info = new Properties();
info.load(is);

String url = info.getProperty("url");
String user = info.getProperty("user");
String password = info.getProperty("password");
String driverClass = info.getProperty("driverClass");

Class.forName(driverClass);

Connection connection = DriverManager.getConnection(url, user, password);

return connection;
}

public static void closeConnection(Connection conn, Statement ps){
try {
if (ps != null) {
ps.close();
}
} catch (SQLException e) {
e.printStackTrace();
}

try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

Statement

  • 通过调用Connection对象的createStatement()方法创建该对象。该对象用于执行静态的SQL语句,并且返回执行结果

  • Statement接口中定义了下列方法用于执行SQL语句

    1
    2
    int excuteUpdate(String sql) // 执行更新操作INSERT、UPDATE、DELETE
    ResultSet executeQuery(String sql) // 执行查询操作SELECT
  • 使用Statement操作数据表存在弊端

    • 问题一:存在拼串操作,繁琐
    • 问题二:存在SQL注入问题
  • SQL注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令

  • 对于Java而言,要防范SQL注入,只要用PreparedStatement(从Statement扩展而来)取代Statement就可以

PreparedStatement

  • PreparedStatement接口是Statement的子接口,它表示一条预编译过的 SQL 语句
  • 可以通过调用Connection对象的preparedStatement(String sql)方法获取PreparedStatement对象
  • PreparedStatement对象所代表的 SQL 语句中的参数用问号(?)来表示,调用PreparedStatement对象的setXxx()方法来设置这些参数,setXxx()方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从 1 开始),第二个是设置的 SQL 语句中的参数的值

PreparedStatement 与 Statement

  • 代码的可读性和可维护性高
  • PreparedStatement能最大可能提高性能
  • PreparedStatement可以防止SQL注入

插入数据的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void testInsert() {
Connection connection = null;
PreparedStatement ps = null;

try {
// 获取链接
connection = JDBCUtils.getConnection();

// 预编译sql语句,返回PreparedStatement实例
String sql = "insert into customers(name,email,birth) value(?,?,?)";
ps = connection.prepareStatement(sql);

// 填充占位符
ps.setString(1,"哪吒");
ps.setString(2,"nezha@gmail.com");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
java.util.Date parse = sdf.parse("2001-03-21");
long time = parse.getTime();
// 这个Date是sql中的Date
ps.setDate(3,new Date(time));

// 执行操作
ps.execute();
} catch (Exception e) {
e.printStackTrace();
}
// 资源释放
JDBCUtils.closeConnection(connection,ps);
}

通用增删改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void update(String sql, Object ...obj){
Connection conn = null;
PreparedStatement ps = null;
try {
// 获取链接
conn = JDBCUtils.getConnection();

// 预编译sql语句,返回PreparedStatement实例
ps = conn.prepareStatement(sql);

// 填充占位符
for(int i=0;i < obj.length; i++){
ps.setObject(i+1,obj[i]);
}

// 执行操作
ps.execute();// 如果需要得到更新后影响的行数,可以改为executeUpdate方法,然后修改返回值类型和增加异常时的输出即可
} catch (Exception e) {
e.printStackTrace();
}finally {
// 释放资源
JDBCUtils.closeConnection(conn,ps);
}
}

通用查询

查询结果是一条记录的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class CommonQuery {
public <T> T getInstance(Class<T> clazz, String sql, Object...obj){
Connection conn = null;
PreparedStatement ps = null;
ResultSet resultSet = null;

try {
// 获取数据库连接
conn = JDBCUtils.getConnection();

// 预编译sql语句,返回PreparedStatement实例
ps = conn.prepareStatement(sql);

// 填充占位符
for (int i = 0; i < obj.length; i++) {
ps.setObject(i+1,obj[i]);
}

// 执行查询操作
resultSet = ps.executeQuery();

// 获取结果集描述对象,从而获得结果集列数
ResultSetMetaData metaData = ps.getMetaData();
int columnCount = metaData.getColumnCount();

// 处理结果集
if (resultSet.next()){
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
Object object = resultSet.getObject(i + 1);
String columnName = metaData.getColumnName(i + 1);// 获取当前列名
Field declaredField = clazz.getDeclaredField(columnName);// 由列名反射获取对应属性
declaredField.setAccessible(true);
declaredField.set(t, object);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
// 关闭资源
JDBCUtils.closeConnection(conn,ps,resultSet);
}
return null;
}
// 测试
@Test
public void test(){
String sql = "select name,email,birth from customers where id=?";
Customer instance = getInstance(Customer.class, sql, 5);
System.out.println(instance);
}
}

查询结果是多条记录的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class CommonQuery {
public <T> List<T> getInstances(Class<T> clazz, String sql, Object...obj){
Connection conn = null;
PreparedStatement ps = null;
ResultSet resultSet = null;

try {
// 获取数据库连接
conn = JDBCUtils.getConnection();

// 预编译sql语句,返回PreparedStatement实例
ps = conn.prepareStatement(sql);

// 填充占位符
for (int i = 0; i < obj.length; i++) {
ps.setObject(i+1,obj[i]);
}

// 执行查询操作
resultSet = ps.executeQuery();

// 获取结果集描述对象,从而获得结果集列数
ResultSetMetaData metaData = ps.getMetaData();
int columnCount = metaData.getColumnCount();

// 创建用于返回的集合
ArrayList<T> list = new ArrayList<>();

// 处理结果集
while (resultSet.next()){
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
Object object = resultSet.getObject(i + 1);
String columnName = metaData.getColumnLabel(i + 1);// 注意此处getColumnLabel与getColumnName的区别
Field declaredField = clazz.getDeclaredField(columnName);
declaredField.setAccessible(true);
declaredField.set(t,object);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
}finally {
// 关闭资源
JDBCUtils.closeConnection(conn,ps,resultSet);
}
return null;
}

@Test
public void test(){
String sql="select name,email,birth from customers where id>?";
List<Customer> instances = getInstances(Customer.class, sql, 10);
System.out.println(instances);

String sql1 = "select order_id id,order_name name,order_date date from `order` where order_id>?";
List<Order> instances1 = getInstances(Order.class, sql1,0);
System.out.println(instances1);
}
}

ResultSet 和 ResultSetMetaData

ResultSet

  • 查询需要调用PreparedStatement 的 executeQuery() 方法,查询结果是一个ResultSet 对象
  • ResultSet 对象以逻辑表格的形式封装了执行数据库操作的结果集,ResultSet 接口由数据库厂商提供实现
  • ResultSet 返回的实际上就是一张数据表。有一个指针指向数据表的第一条记录的前面
  • ResultSet 对象维护了一个指向当前数据行的游标,初始的时候,游标在第一行之前,可以通过 ResultSet 对象 的 next() 方法移动到下一行。调用 next()方法检测下一行是否有效。若有效,该方法返回 true,且指针下移。 相当于Iterator对象的 hasNext() 和 next() 方法的结合体。 当指针指向一行时, 可以通过调用 getXxx(int index) 或 getXxx(int columnName) 获取每一列的值。 例如: getInt(1), getString(“name”)
  • 注意Java与数据库交互涉及到的相关Java API中的索引都从1开始

ResultSetMetaData

  • 可用于获取关于ResultSet对象中列的类型和属性信息的对象
  • 通过ResultSet的getMetaData()方法获取
  • 常用方法
    • getColumnName(int column)方法,获取指定列的名称
    • getColumnLabel(int column)方法,获取指定列的别名,如果不指定别名则返回名称
    • getColumnCount()方法,返回当前 ResultSet 对象中的列数

资源释放

  • 释放ResultSet, Statement,Connection。
  • 数据库连接(Connection)是非常稀有的资源,用完后必须马上释放,如果Connection不能及时正确的关闭将导致系统宕机
  • Connection的使用原则是尽量晚创建,尽量早的释放。 可以在finally中关闭,保证及时其他代码出现异常,资源也一定能被关闭。

操作BLOB类型字段

MySQL中的BLOB类型字段

  • MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据
  • 插入BLOB类型的数据必须使用PreparedStatement,因为BLOB类型的数据无法使用字符串拼接写的
  • MySQL的四种BLOB类型
    1. TinyBlob,最大存放255B
    2. Blob,最大存放65KB
    3. MediumBlob,最大存放16MB
    4. LongBlob,最大存放4GB
  • 实际使用中根据需要存入的数据大小定义不同的BLOB类型
  • 需要注意的是:如果存储的文件过大,数据库的性能会下降
  • 如果在指定了相关的Blob类型以后,还报错:xxx too large,那么在mysql的安装目录下,找my.ini文件加上如 下的配置参数: max_allowed_packet=16M。同时注意:修改了my.ini文件之后,需要重新启动mysql服务

插入Blob类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取连接
Connection conn = JDBCUtils.getConnection();
String sql = "insert into customers(name,email,birth,photo)values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);

// 填充占位符
ps.setString(1, "CCY");
ps.setString(2, "CCY@126.com");
ps.setDate(3, new Date(new java.util.Date().getTime()));

// 操作Blob类型的变量
FileInputStream fis = new FileInputStream("ccy.png");
ps.setBlob(4, fis);

// 执行
ps.execute();
fis.close();
JDBCUtils.closeResource(conn, ps)

查询Blob类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
String sql = "SELECT id, name, email, birth, photo FROM customer WHERE id = ?";

conn = getConnection();
ps = conn.prepareStatement(sql);
ps.setInt(1, 8);
rs = ps.executeQuery();

if(rs.next()){
Integer id = rs.getInt(1);
String name = rs.getString(2);
String email = rs.getString(3);
Date birth = rs.getDate(4);
Customer cust = new Customer(id, name, email, birth);
System.out.println(cust);
// 读取Blob类型的字段
Blob photo = rs.getBlob(5);
InputStream is = photo.getBinaryStream();// 从查询出来的Blob对象中获取二进制流
OutputStream os = new FileOutputStream("c.jpg");
byte [] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
JDBCUtils.closeResource(conn, ps, rs);
if(is != null){
is.close();
}
if(os != null){
os.close();
}

批量插入

批量执行SQL语句

  • 当需要成批插入或者更新记录时,可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率

  • JDBC的批量处理语句包括下面三个方法

    | 方法 | 说明 |
    | —————————- | ————————————————- |
    | addBatch(String) | 添加需要批量处理的SQL语句或是参数 |
    | executeBatch() | 执行批量处理语句 |
    | clearBatch() | 清空缓存的数据 |

  • 两种批量执行SQL语句的情况

    • 多条SQL语句的批量处理
    • 一个SQL语句的批量传参

高效的批量插入

1.使用Statement

1
2
3
4
5
6
Connection conn = JDBCUtils.getConnection();
Statement st = conn.createStatement();
for(int i = 1;i <= 20000;i++){
String sql = "insert into goods(name) values('name_' + "+ i +")";
st.executeUpdate(sql);
}

2.使用PreparedStatement(比Statement高效)

1
2
3
4
5
6
7
8
9
10
11
long start = System.currentTimeMillis();
Connection conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 20000;i++){
ps.setString(1, "name_" + i);
ps.executeUpdate();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//82340
JDBCUtils.closeResource(conn, ps);

3.使用批量处理语句对2.优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testInsert1() throws Exception{
long start = System.currentTimeMillis();
Connection conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 1000000;i++){
ps.setString(1, "name_" + i);
//1.“攒”sql
ps.addBatch();
if(i % 500 == 0){
//2.执行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//20000条:625
//1000000条:14733
JDBCUtils.closeResource(conn, ps);
}

4.使用Connection的setAutoCommit()方法对3.优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void testInsert2() throws Exception{
long start = System.currentTimeMillis();
Connection conn = JDBCUtils.getConnection();
//1.设置为不自动提交数据
conn.setAutoCommit(false);
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 1000000;i++){
ps.setString(1, "name_" + i);
//1.“攒”sql
ps.addBatch();
if(i % 500 == 0){
//2.执行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}
//2.手动提交数据
conn.commit();
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//1000000条:4978
JDBCUtils.closeResource(conn, ps);
}

数据库事务

事务介绍

  • 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态
  • 事务处理(事务操作):保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方 式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态
  • 为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可 以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态

JDBC中对事务的处理

  • 数据一旦提交,就不可回滚

  • 数据什么时候意味着提交?

    • 当一个连接对象被创建时,默认情况下是自动提交事务:每次执行一个 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
    • 关闭数据库连接,数据就会自动的提交。如果多个操作,每个操作使用的是自己单独的连接,则无法保证 事务。即同一个事务的多个操作必须在同一个连接下
  • JDBC程序中为了让多个 SQL 语句作为一个事务执行

    • 调用Connection对象的setAutoCommit(false);以取消自动提交事务
    • 在所有的SQL语句都成功执行后,调用commit();方法提交事务
    • 在出现异常时,调用rollback();方法回滚事务
    • 在使用数据库连接池技术时,执行close()方法前,建议使用setAutoCommit(true);恢复自动提交状态
  • 使用事务后的通用增删改查操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public void update(Connection conn,String sql,Object ...obj){
    PreparedStatement ps = null;
    try {
    // 预编译sql语句,返回PreparedStatement实例
    ps = conn.prepareStatement(sql);
    // 填充占位符
    for(int i=0;i<obj.length;i++){
    ps.setObject(i+1,obj[i]);
    }
    // 执行操作
    ps.execute();
    } catch (Exception e) {
    e.printStackTrace();
    }finally {
    // 释放资源
    JDBCUtils.closeConnection(null,ps);
    }
    }
  • 事务使用实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public void testJDBCTransaction() {
    Connection conn = null;
    try {
    // 获取数据库连接
    conn = JDBCUtils.getConnection();
    // 开启事务
    conn.setAutoCommit(false);
    // 进行数据库操作
    String sql1 = "update user_table set balance = balance - 100 where user = ?";
    update(conn, sql1, "AA");
    // 模拟网络异常
    // System.out.println(10 / 0);
    String sql2 = "update user_table set balance = balance + 100 where user = ?";
    update(conn, sql2, "BB");
    // 若没有异常,则提交事务
    conn.commit();
    } catch (Exception e) {
    e.printStackTrace();
    // 若有异常,则回滚事务
    try {
    conn.rollback();
    } catch (SQLException e1) {
    e1.printStackTrace();
    }
    } finally {
    try {
    // 恢复每次DML操作的自动提交功能
    conn.setAutoCommit(true);
    } catch (SQLException e) {
    e.printStackTrace();
    }
    // 关闭连接
    JDBCUtils.closeResource(conn, null, null);
    }
    }

事务的ACID属性

ACID特性 说明
原子性(Atomicity) 指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
一致性(Consistency) 事务必须使数据库从一个一致性状态变换到另外一个一致性状态
隔离性(Isolation) 指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的 数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
持久性(Durability) 指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其 他操作和数据库故障不应该对其有任何影响

并发问题

  • 对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题
并发问题 说明
脏写 Dirty Write 对于两个事务T1、T2,T1修改了另一个未提交的事务T2修改过的数据
脏读 Dirty Read 对于两个事务T1、T2,T1读取了已经被T2更新但还没有被提交的字段。之后, 若 T2 回滚,T1读取的内容就是临时且无效的
不可重复读 Non-Repeatable Read 对于两个事务T1、T2,T1读取了一个字段, 然后 T2 更新了该字段。之后,T1再次读取同一个字段,值就不同了
幻读 Phantom 对于两个事务T1、T2,T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行。之后,如果T1再次读取同一个表,就会多出几行
  • 数据库事务的隔离性: 数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题

  • 一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱

隔离级别

隔离级别 说明
READ UNCOMMITED(读未提交) 允许事务读取其他事务未提交的变更,会出现脏读、不可重复读、幻读的问题
READ COMMITTED(读已提交) 只允许事务读取已经被其他事务提交的变更,可以避免脏读,但存在不可重复读、幻读的问题
REPETABLE READ(可重复读) 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,避免了脏读和不可重复读,但存在幻读问题
SERIALIZABLE(串行化) 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新、删除的操作,解决所有的并发问题,但是性能低下
  • Oracle支持的2种事务隔离级别:READ COMMITED, SERIALIZABLE。Oracle 默认的事务隔离级别为: READ COMMITED
  • Mysql 支持4种事务隔离级别。Mysql默认的事务隔离级别为: REPEATABLE READ

数据库连接池

JDBC数据库连接池的必要性

在使用开发基于数据库的web程序时,传统的模式基本是按以下步骤

  1. 在主程序(如servlet、beans)中建立数据库连接
  2. 进行sql操作
  3. 断开数据库连接

这种模式开发,存在的问题

  1. 普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码。需要数据库连接的时候,就向数据库要求 一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很 好的重复利用。若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃
  2. 对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库
  3. 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃

数据库连接池技术

  • 为解决传统开发中的数据库连接问题,可以采用数据库连接池技术

  • 基本思想:为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去

  • 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重 新建立一个

  • 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库 连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池 的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连 接数量时,这些请求将被加入到等待队列中

    技术特点

    • 资源重用

      由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一 方面也增加了系统运行环境的平稳性

    • 更快的系统反应速度

      数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均 已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销, 从而减少了系统的响应时间

    • 新的资源分配手段

      对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库 连接数的限制,避免某一应用独占所有的数据库资源

    • 统一的连接管理,避免数据库连接泄漏

      在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据 库连接操作中可能出现的资源泄露

开源数据库连接池

  • JDBC 的数据库连接池使用javax.sql.DataSource来表示,DataSource 只是一个接口,该接口通常由服务器 (Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现
    • C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
    • Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池,但是 速度不确定是否有BoneCP快
  • DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把DataSource称为连接池
  • DataSource用来取代DriverManager来获取Connection,获取速度快,同时可以大幅度提高数据库访问速度
  • 特别注意
    • 数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个 数据源即可
    • 当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但conn.close()并没有关闭数 据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池

C3P0

1
2
3
4
5
6
// 使用C3P0数据库连接池的配置文件方式,获取数据库的连接:推荐
private static DataSource cpds = new ComboPooledDataSource("helloc3p0");
public static Connection getConnection() throws SQLException{
Connection conn = cpds.getConnection();
return conn;
}

其中src下的配置文件c3p0-config.xml如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<named-config name="helloc3p0">
<!-- 获取连接的4个基本信息 -->
<property name="user">root</property>
<property name="password">abc123</property>
<property name="jdbcUrl">jdbc:mysql:///test</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>

<!-- 涉及到数据库连接池的管理的相关属性的设置 -->
<!-- 若数据库中连接数不足时, 一次向数据库服务器申请多少个连接 -->
<property name="acquireIncrement">5</property>
<!-- 初始化数据库连接池时连接的数量 -->
<property name="initialPoolSize">5</property>
<!-- 数据库连接池中的最小的数据库连接数 -->
<property name="minPoolSize">5</property>
<!-- 数据库连接池中的最大的数据库连接数 -->
<property name="maxPoolSize">10</property>
<!-- C3P0 数据库连接池可以维护的 Statement 的个数 -->
<property name="maxStatements">20</property>
<!-- 每个连接同时可以使用的 Statement 对象的个数 -->
<property name="maxStatementsPerConnection">5</property>
</named-config>
</c3p0-config>

Druid

1
2
3
4
5
6
7
8
9
public class TestDruid {
public static void main(String[] args) throws Exception {
Properties pro = new Properties();
pro.load(TestDruid.class.getClassLoader().getResourceAsStream("druid.properties"));
DataSource ds = DruidDataSourceFactory.createDataSource(pro);
Connection conn = ds.getConnection();
System.out.println(conn);
}
}

其中,src下的配置文件为:【druid.properties】

1
2
3
4
5
6
7
8
9
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
username=root
password=123456
driverClassName=com.mysql.jdbc.Driver

initialSize=10
maxActive=20
maxWait=1000
filters=wall

详细配置参数

配置 缺省 说明
name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过 名字来区分开来。 如果没有配置,将会生成一个名字,格式 是:”DataSource-” + System.identityHashCode(this)
url 连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 连接数据库的用户名
password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用 ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动 识别dbType,然后选择相应的driverClassName(建议配置下)
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者 第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启 用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock属性为true使用非公平锁
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游 标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭
maxOpenPreparedStatements -1 要启用PSCache,必须配置大于0,当大于0时, poolPreparedStatements自动触发修改为true。在Druid中,不会存 在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一 些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句。如果 validationQuery为null,testOnBorrow、testOnReturn、 testWhileIdle都不会其作用
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会 降低性能
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会 降低性能
testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检 测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行 validationQuery检测连接是否有效
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall