Fork me on GitHub

数据库(4)-事务与数据库连接池

此处输入图片的描述
本文总结数据库事务安全问题与隔离级别,以及数据库连接池的相关知识。

一、MySQL事务(Transaction)

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务。

MYSQL 事务处理主要有两种方法:

  • 1、用 BEGIN, ROLLBACK, COMMIT来实现

    1
    2
    3
    4
    BEGIN 开始一个事务
    //START TRANSACTION
    ROLLBACK 事务回滚
    COMMIT 事务确认
  • 2、直接用 SET 来改变 MySQL 的自动提交模式:

    1
    2
    SET AUTOCOMMIT=0 禁止自动提交
    SET AUTOCOMMIT=1 开启自动提交

在JDBC代码中,需要有两个声明:

1
2
3
4
5
6
7
8
9
//调用Connection对象的setAutoCommit();
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/database_name", "user", "password");
conn.setAutoCommit(false);
//随后执行数据库操作
...
//最后提交
conn.commit()
//并且在异常处理中回滚
conn.rollback();

注意:setAutoCommit()只针对Connection对象,如果新建一个连接对象,仍然是默认提交。

二、事务的特性

事务是必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

三、数据库的安全问题和隔离级别

(1)安全问题-读

脏读、 不可重复读、幻读

脏读:一个事务读到了另一个事务未提交的数据。

不可重复读:一个事务读到了另一个事务提交的数据,多次查询结果不一致。

幻读:一个事务读到了另一个事务已提交的插入的数据,导致多次查询结果不一致。

(2)安全问题-写

丢失更新问题,指一个事务去修改数据库, 另一个事务也修改数据库,最后的那个事务,不管是提交还是回滚都会造成前面一个事务的数据更新丢失

主要解决办法是加锁:乐观锁、悲观锁

悲观锁:指事务在一开始就认为丢失更新一定会发生, 这是一件很悲观的事情。类似synchronized加锁。悲观锁通过在查询条件后面加for udate实现。

乐观锁:指从来不会觉得丢失更新会发生。通过手动在数据库中设置字段,利用CAS操作实现。

(3)不可重复读和幻读的区别

幻读更加关注于insert操作,事务在插入已经检查过不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测获取到的数据如同鬼影一般。

(4)隔离级别-四种

读未提交:Read Uncommitted

引发问题: 脏读

读已提交:Read Committed

解决: 脏读 , 引发: 不可重复读

可重复读:Repeatable Read

解决: 脏读 、 不可重复读 , 未解决: 幻读

可串行化:Serializable

解决: 脏读、 不可重复读 、 幻读。

mySql 默认的隔离级别是 可重复读
Oracle 默认的隔离级别是 读已提交

(5)设置隔离级别

1
2
3
4
5
6
7
8
//读未提交
set session transaction isolation level read uncommited;
//读已提交
set session transaction isolation level read commited;
//可重复读
set session transaction isolation level repeatable read;
//可串行化
set session transaction isolation level serializable;

四、数据库连接池

(1)作用

1. 资源重用

由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。

2. 更快的系统响应速度

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

3. 新的资源分配手段

对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术,几年钱也许还是个新鲜话题,对于目前的业务系统而言,如果设计中还没有考虑到连接池的应用,那么…….快在设计文档中加上这部分的内容吧。某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。

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

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

(2)常用数据库连接池

常用的数据库连接池包括DBCP,C3P0,Druid
关键配置:

最小连接数:

是数据库一直保持的数据库连接数,所以如果应用程序对数据库连接的使用量不大,将有大量的数据库资源被浪费。

初始化连接数:

连接池启动时创建的初始化数据库连接数量。

最大连接数:

是连接池能申请的最大连接数,如果数据库连接请求超过此数,后面的数据库连接请求被加入到等待队列中。

最大等待时间:

当没有可用连接时,连接池等待连接被归还的最大时间,超过时间则抛出异常,可设置参数为0或者负数使得无限等待(根据不同连接池配置)。

A. DBCP使用

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
public void testDBCP01(){
Connection conn = null;
PreparedStatement ps = null;
try {
//1. 构建数据源对象
BasicDataSource dataSource = new BasicDataSource();
//连的是什么类型的数据库, 访问的是哪个数据库 , 用户名, 密码。。
//jdbc:mysql://localhost/bank 主协议:子协议 ://本地/数据库
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost/databasename");
dataSource.setUsername("root");
dataSource.setPassword("root");
//2. 得到连接对象
conn = dataSource.getConnection();
String sql = "insert into account values(null , ? , ?)";
ps = conn.prepareStatement(sql);
ps.setString(1, "admin");
ps.setInt(2, 1000);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
//3.释放连接对象
JDBCUtil.release(conn, ps);
}
}

B.C3P0使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Connection conn = null;
PreparedStatement ps = null;
try {
//1. 创建datasource
ComboPooledDataSource dataSource = new ComboPooledDataSource();
//2. 设置连接数据的信息
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost/database_name");
dataSource.setUser("root");
dataSource.setPassword("root");
//3. 得到连接对象
conn = dataSource.getConnection();
String sql = "insert into account values(null , ? , ?)";
ps = conn.prepareStatement(sql);
ps.setString(1, "admin");
ps.setInt(2, 100);
ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放资源
JDBCUtil.release(conn, ps);
}

注意,在使用c3p0连接池的时候,可能遇到无法连接的问题,而此时,可以尝试将配置文件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
25
26
27
28
29
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<!-- 主要注意这行内容,需要用 &amp; 代替 & -->
<property name="jdbcUrl">jdbc:mysql://localhost:3306/stu?serverTimezone=GMT&amp;characterEncoding=UTF-8&amp;useSSL=false</property>
<property name="user">root</property>
<property name="password">root</property>

<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>

<named-config name="otherc3p0">
<!-- 连接参数 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/??</property>
<property name="user">root</property>
<property name="password">root</property>

<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">8</property>
<property name="checkoutTimeout">1000</property>
</named-config>
</c3p0-config>

(3)自定义数据库连接池

使用来存放数据库连接,每次都从上面取出连接,使用完也放回上面。假如使用的频率特别低会导致栈底部的连接长时间未使用,则可以直接释放以节省资源。

连接容器中超时连接的释放有两种方式:
(1)在往容器中添加或者取出连接的时候释放。
(2)单独开一个线程不断轮询所有连接释放超时的连接。一般采用第一种方式。

栈中连接的使用时间是有序的。所以每次释放的时候,只需要从底部向上开始扫描,遇到超时的连接则进行释放,遇上非超时的连接则停止扫描,如果栈中连接均未超时,则只需要扫描最后一个就可以了。


参考文献
常用数据库连接池 (DBCP、c3p0、Druid) 配置说明
数据库连接池的实现及原理

-------------本文结束感谢阅读-------------