`

day14 JDBC事务管理 数据库连接池技术

 
阅读更多


今天学习重点:JDBC事务管理、数据库连接池技术

事务:一组全部成功、全部失败操作。(这组操作不可分割)
案例:转账案例

MySQL 数据库 默认情况下 一条SQL就是一个单独事务,事务是自动提交的
Oracle 数据库 默认情况下 事务不是自动提交 ,所有SQL都将处于一个事务中,你需要手动进行commit提交/rollback回滚

设计账户table
create table account(
    id int primary key not null,
    name varchar(40),
    money double
);

insert into account values(1,'aaa',1000);
insert into account values(2,'bbb',1000);
insert into account values(3,'ccc',1000);

在mysql管理事务
start transaction  开启事务 (所有对数据表增加、修改、删除操作 临时表进行)
rollback  回滚事务 (取消刚刚操作)
commit   提交事务 (确认刚才操作)
* 在事务管理中执行sql,使用数据库内临时表保存,在没有进行事务提交或者回滚,其它用户无法看到事务操作结果的
* SQL语言中只有 DML才能被事务管理 insert update delete

Oracle实验
create table account(
    id int primary key not null,
    name varchar(40),
    money number
);

使用JDBC程序如何控制事务
Connection.setAutoCommit(false); //  相当于start transaction
Connection.rollback();  rollback --- 回滚到事务开启时状态
Connection.commit();  commit

* 将mysql的jar 复制 WEB-INF/lib
* 复制之前编写的JDBCUtils工具类
* 将数据库配置文件 dbconfig.properties 复制 src目录下 ----- 修改数据库配置

事务回滚点 SavePoint
* 当时事务特别复杂,有些情况不会回滚到事务最开始状态,需要将事务回滚到指定位置
Savepoint sp = conn.setSavepoint(); 设置回滚点
Conn.rollback(sp); 事务回滚到指定位置

create table person(
   id int primary key,
   name varchar(40)
);
例子:
public void demo3() {
  // 创建person表,向表中插入20000条数据 ------ 如果插入过程中发生错误,插入数据条数1000整数倍
  // PreparedStatement 批处理
  Connection conn = null;
  PreparedStatement stmt = null;
  Savepoint savepoint = null;

  try {
   conn = JDBCUtils.getConnection();
   // 开启事务
   conn.setAutoCommit(false);
   // 保存一次
   savepoint = conn.setSavepoint();

   String sql = "insert into person values(?,?)";
   // 预编译SQL
   stmt = conn.prepareStatement(sql);

   for (int i = 1; i <= 20000; i++) {
    stmt.setInt(1, i);
    stmt.setString(2, "name" + i);

    // 添加到批处理
    stmt.addBatch();

    if (i == 4699) {
     int d = 1 / 0;
    }

    // 每隔200向数据库发送一次
    if (i % 200 == 0) {
     stmt.executeBatch();
     stmt.clearBatch();
    }

    if (i % 1000 == 0) {// 1000整数倍
     // 保存 回滚点
     savepoint = conn.setSavepoint();
    }
   }

   stmt.executeBatch();// 为了确保缓存sql都提交了

   // 没有错误
   conn.commit();

  } catch (Exception e) {
   // 回滚事务,回滚存储点
   try {
    conn.rollback(savepoint);
    conn.commit();
   } catch (SQLException e1) {
    e1.printStackTrace();
   }
   e.printStackTrace();
  } finally {
   JDBCUtils.release(stmt, conn);
  }
 }
----------------------------------------------------------------------------------------------
事务的四大特性: ACID  原子性、一致性、隔离性、持久性
原子性:事务的一组操作不可分割,要么都成功,要么都失败
一致性:事务前后数据完整性 转账前 A 和 B 账户总和2000元,转账后 总和还是2000 元
隔离性:并发访问存在时,事务之间是隔离的,一个事务不应该影响其它事务运行效果
持久性:当事务一旦提交,事务数据永久存在,无法改变

企业开发中一定要保证事务原子性,事务最复杂问题都是由事务隔离性引起的

不考虑事务隔离将引发哪些问题:脏读、不可重复读、虚读
脏读:一个事务读取另一个事务 未提交数据 ---- 是数据库隔离中最重要问题
不可重复读:一个事务读取另一个事务 已提交数据,在一个事务中两次读取结果不同 ----- 在某些情况下出现问题
虚读:一个事务读取另一个事务 插入数据,造成在一个事务中两次读取记录条数不同
* 虚读 不可重复读 区别? 不可重复读读取 update数据 ,虚读读取insert 数据

数据库为了解决三类隔离引发问题:提供四个数据库隔离级别(所有数据库通用)
Serializable : 串行处理  ---- 解决三类问题
Repeatable read :可以解决 不可重复读、脏读,会发生虚读   ------- MySQL 默认级别
read committed : 可以 解决脏读 ,会发生 不可重复读、虚读  -------- Oracle默认级别
read uncommitted : 会导致三类问题发生
Serializable  > Repeatable read > read committed > read uncommitted
数据库隔离问题危害 脏读> 不可重复读 > 虚读

安全级别越高,处理效率越低;安全级别越低,效率高

在数据库中通过
set transaction isolation level 设置事务隔离级别
select @@tx_isolation 查询当前事务隔离级别

小实验:将演示各个级别导致隔离问题 --- 默认并发访问
打开两个mysql 窗口

1、脏读问题 read uncommitted ----
将B窗口隔离级别设置 read uncommitted
set transaction isolation level read uncommitted;
在A、B窗口分别开启一个事务 start transaction
在A窗口完成转账操作
update account set money= money - 200 where name='aaa';
update account set money= money +200 where name='bbb';

在B窗口进行查询 ---- 读取到未提交转账结果
A创建回滚 rollback B窗口结果 恢复之前

2、不可重复读 read committed
将B窗口隔离级别设置 read committed
set transaction isolation level read committed;
重复刚才操作

在A创建没有提交前,B窗口查询数据不会改变  (避免脏读)
A窗口提交 commit
B窗口读取A窗口提交结果 (在同一个事务中 发生不可重复读)

3、虚读 Repeatable read
将B窗户 隔离级别设置 Repeatable read
set transaction isolation level Repeatable read;
在A、B创建分别开启事务
在A窗口转账 提交
B 连续查询,会发现不会读取到 A提交 数据结果 (避免不可重复读)

* 虚读发生概率很低
A窗口插入一条数据 B窗口能够在同一个事务查询到 --- 虚读

4、演示 Serializable  串行处理效果
将B窗口级别设置 Serializable 
set transaction isolation level Serializable;
在A、B窗口同时开启事务

在B窗口查询,在A窗口插入 --- 发现A窗口阻塞
* 当B窗口操作表数据时,别窗口无法操作

************************
在JDBC程序中如何控制数据库隔离级别  Connection setTransactionIsolation(int level)
* 如果不设置隔离级别---- 采用数据库默认

* 64位mysql5.5 无法修改隔离级别 set session transaction isolation level read committed;

----------------------------------------------------------------------------------------------
事务丢失更新问题
两个事务同时读取同一条记录,A先修改记录,B也修改记录(B是不知道A修改过),B提交数据后B的修改结果覆盖了A的修改结果。

解决丢失更新:通过悲观锁 和 乐观锁

1、悲观锁原理,使用数据库内部锁机制,进行table的锁定,在A修改数据时,A就将数据锁定,B此时无法进行修改 ----- 无法发生两个事务同时修改
* 假设丢失更新会发生

在mysql中默认情况下,当你修改数据,自动为数据加锁(在事务中) ---- 防止两个事务同时修改数据   ---- 读锁
* 事务和锁和不可分开的,锁一定是在事务中使用 ,当事务关闭锁自动释放

在mysql内部有两种常用锁 读锁和写锁
读锁(共享锁) 一张表可以添加多个读锁,如果表添加读锁(不是当前事务添加的),该表不可以修改
* select * from account lock in share mode;
* 共享锁非常容易发生死锁
写锁(排它锁) 一张表只能加一个排它锁,排他锁和其它共享锁、排它锁都具有互斥效果 。
* 如果一张表想添加排它锁,前提之前表一定没有加过共享锁和排他锁
* select * from account for update ;

悲观锁可以使用 排它锁实现 ----- 解决丢失更新问题

2、乐观锁原理: 使用不是数据库锁机制,而是一个特殊标记字段,如果控制字段状态和内容,得知数据是否发生并发访问!
* 假设丢失更新不会发生
* 数据库timestamp 时间戳字段
create table blog (
  id int primary key,
  title varchar(40),
  updatetime timestamp
);

insert into blog values(1,'java学习',null); ---- timestamp在数据插入时,字段生成当前时间
update blog set title = '传智播客考试' where id =1 ; ---- timestamp 在数据修改时,自动更新为当前时间

------------------------------------------------------------------------------------------------------
数据库开发中存在问题,每次客户请求,在服务器端都单独创建一个连接操作数据库,当并发访问量非常大,很容易造成内存溢出,而且创建连接、释放连接资源非常消耗服务器性能。

连接池原理: 在服务器端一次性创建多个连接,将多个连接保存在一个连接池对象中,当请求需要操作数据库时,不会为请求创建新的连接,而是直接从连接池中获得一个连接,操作数据库结束,并不需要真正关闭连接,而是将连接放回到连接池中。
* 节省创建连接、释放连接 资源

自定义一个连接池
1、编写class 实现DataSource 接口
2、在class构造器 一次性创建10个连接,将连接保存LinkedList中
3、实现getConnection  从 LinkedList中 返回一个连接
4、提供将连接放回连接池中方法
* 当用户使用连接后,不能调用Connection的close,而要使用连接池提供关闭方法,将连接放回连接池

用户调用Connection的 close能否将连接放回连接池呢?
------------------ 修改close方法原来逻辑

Java中常用三种方法可以增强 原有方法
1、类继承 、方法覆盖
* 必须控制对象创建,才能使用该方式
2、装饰者模式方法加强
* 必须和目标对象实现相同接口或继续相同父类,特殊构造器(传入被包装对象)
3、动态代理

-----------------------------------------------------------------------------------------------------------
在实际开发中 不会自己实现连接池 ,使用开源免费数据库连接池
Apache commons-dbcp 连接池
c3p0 数据库连接池
Tomcat内部提供数据库连接池

1、当使用Apache DBCP 需要下载 commons-dbcp.jar commons-pool.jar
* apache commons 子项目 zip包没有快速入门文档 只有API
手动设置四个参数
编写properties配置文件 ------ Properties对象加载文件

public class DBCPUtils {
 private static DataSource ds = null;
 static {
  try {
   InputStream in = DBCPUtils.class.getClassLoader()
     .getResourceAsStream("dbcpconfig.properties");
   Properties prop = new Properties();
   prop.load(in);
   ds = BasicDataSourceFactory.createDataSource(prop);
  } catch (Exception e) {
   throw new ExceptionInInitializerError(e);
  }
 }
 public static Connection getConnection() throws SQLException{
  return ds.getConnection();
 }
 public static DataSource getSource(){
  return ds;
 }
}

2、
导入c3p0-0.9.1.2.jar
下载c3p0 解压 doc目录 存在c3p0 使用入门
ComboPooledDataSource 手动设置参数
配置文件 在src目录新建 c3p0-config.xml
<default-config>
<named-config name="intergalactoApp"> 自定义配置可以有很多个
* 在实际软件系统中,测试环境、开发环境、线上数据库 是不同数据库

Basic Pool Configuration 基本属性
    acquireIncrement  当连接池连接用完了,根据该属性决定一次性新建多少连接
    initialPoolSize  初始化一次性创建多少个连接
    maxPoolSize 最大连接数
    maxIdleTime 最大空闲时间,当连接池中连接经过一段时间没有使用,根据该数据进行释放
    minPoolSize 最小连接池尺寸

当创建连接池时,一次性创建initialPoolSize 个连接,当连接使用完一次性创建 acquireIncrement  个连接,连接最大数量 maxPoolSize ,当连接池连接数量大于 minPoolSize ,经过maxIdleTime 连接没有使用, 该连接将被释放

ComboPooledDataSource dataSource = new ComboPooledDataSource("自定义配置名称");
* 如果不存在配置,将使用默认配置

public class JDBCUtils {
 private static DataSource dataSource = new ComboPooledDataSource();

 // 返回连接池,将连接池交给框架,框架自动获得连接 管理事务
 public static DataSource getDataSource() {
  return dataSource;
 }

 // 提供DBUtils 手动控制事务使用
 public static Connection getConnection() throws SQLException {
  return dataSource.getConnection();
 }
}

3、Tomcat内置连接池
因为tomcat和 dbcp 都是Apache公司项目,tomcat内部连接池就是dbcp
* Tomcat 支持Servlet/JSP 容器 ,并不支持所有JavaEE 规范 ------- JNDI
开发者通过JNDI方式 访问Tomcat内置 连接池

将web工程部署到tomcat 三种方式: 配置server.xml <Context> 元素、配置独立xml文件 <Context> 元素 、直接将网站目录复制Tomcat/webapps
虚拟目录 ---- <Context> 元素

<Context>
<Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
               maxActive="100" maxIdle="30" maxWait="10000"
               username="root" password="123" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/day14"/>
</Context>

在哪里配置<Context> ???三个位置
1) tomcat安装目录/conf/context.xml --------- 对当前tomcat内部所有虚拟主机中任何工程都有效
2) tomcat安装目录/conf/Catalina/虚拟主机目录/context.xml -------- 对当前虚拟主机任何工程都有效
3) 在web工程根目录/META-INF/context.xml ------- 对当前工程有效

用JNDI访问连接池! 什么是JNDI ?
1)、将一个Java对象,绑定JNDI容器中,为java对象起一个名字
2)、Java其它程序 通过名字 检索到绑定对象

编写JNDI程序
1)、 将jar复制tomcat/lib
2)、 编写访问JNDI程序 运行Tomcat内部 ---- 通常Servlet、JSP
                        // 创建检索对象
   Context initCtx = new InitialContext();
   // 默认查找顶级java 名称串 固定:java:comp/env
   Context envCtx = (Context) initCtx.lookup("java:comp/env");
   // 根据设置名称 查找连接池对象
   DataSource ds = (DataSource) envCtx.lookup("jdbc/TestDB");


总结:
1、mysql数据库控制事务 start transaction 、commit 、 rollback
* mysql默认事务自动提交,Oracle需要手动提交

2、JDBC程序控制事务 conn.setAutoCommit(false) conn.commit() conn.rollback();
* SavePoint

3、事务四大特性 ACID
4、如果不考虑隔离性 引发三类问题:脏读、不可重复读、虚读
5、数据库四个隔离级别 Serializable / Repeatable read / Read Committed / Read UnCommitted
* 隔离级别实验

6、丢失更新 悲观锁和乐观锁
悲观锁 select * ... for update
乐观锁 时间戳

7、连接池原理
8、三种方法增强 !!!!!!!!!!!!!
继承覆盖、装饰者、动态代理

9、Apache DBCP 、c3p0 、Tomcat内置连接池 使用 ------- c3p0

10、Tomcat内置连接池 --- JNDI原理
JNDI容器启动时,将对象绑定容器中,为对象起名字
JNDI容器中其它程序,可以通过名称 ---- 访问绑定对象

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics