事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功
数据库开启事务命令
start transaction 开启事务
Rollback 回滚事务
Commit 提交事务
1.创建表
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
insert into account values(null,'aaa',1000);
insert into account values(null,'bbb',1000);
insert into account values(null,'ccc',1000);
2、MySQL中事务默认自动提交的,每当执行一条SQL,就会提交一个事务 (一条SQL 就是一个事务)
Oracle 中事务默认 不自动提交,需要在执行SQL 语句后 通过commint 手动提交事务
3、mysql管理事务
方式一 :同时事务管理SQL 语句
start transaction 开启事务
rollback 回滚事务 (将数据恢复到事务开始时状态)
commit 提交事务 (对事务中进行操作,进行确认操作,事务在提交后,数据就不可恢复)
方式二:数据库中存在一个自动提交变量 ,通过 show variables like '%commit%'; ---- autocommint 值是 on,说明开启自动提交
关闭自动提交 set autocommit = off / set autocommit = 0
如果设置autocommit 为 off,意味着以后每条SQL 都会处于一个事务中,相当于每条SQL执行前 都执行 start transaction
补充:Oracle中 autocommit 默认就是 off
当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:
JDBC控制事务语句
Connection.setAutoCommit(false); // 相当于start transaction
Connection.rollback(); rollback
Connection.commit(); commit
示例 演示银行转帐案例
在JDBC代码中使如下转帐操作在同一事务中执行。
update from account set money=money-100 where name=‘a’;
update from account set money=money+100 where name=‘b’;
设置事务回滚点
Savepoint sp = conn.setSavepoint();
Conn.rollback(sp);
Conn.commit(); //回滚后必须要提交
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
如果不考虑隔离性,可能会引发如下问题
1、脏读 :指一个事务读取另一个事务未提交的数据
A 转账 给B 100,未提交
B 查询账户多了100
A 回滚
B 查询账户那100不见了
2、不可重复读:在一个事务先后两次读取发生数据不一致情况,第二次读取到另一个事务已经提交数据 (强调数据更新 update)
A 查询账户 5000
B 向 A 账户转入 5000
A 查询账户 10000
3、虚读(幻读) : 在一个事务中,第二次读取发生数据记录数的不同 ,读取到另一个事务已经提交数据 (强调数据记录变化 insert )
A 第一次读取 存在5条记录
B 向 A 插入一条新的记录
A 第二次读取 存在6条记录
数据库内部定义了四种隔离级别,用于解决三种隔离问题
1 Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)
2 Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读)不可以避免虚读
3 Read committed:可避免脏读情况发生(读已提交)
4 Read uncommitted:最低级别,以上情况均无法保证。(读未提交)
操作数据库内部隔离级别
set session transaction isolation level 设置事务隔离级别
select @@tx_isolation 查询当前事务隔离级别
实验一:演示脏读发生
在A窗口 将隔离级别设置 read uncommitted
A、B窗口同时开启事务
B窗口执行转账操作 update account set money = money - 500 where name='bbb';
update account set money = money + 500 where name ='aaa'; 未提交事务
A窗口查询 select * from account; 查询到转账结果(脏读)
B 回滚 rollback
A窗口查询 金钱丢失
实验二:演示不可重复读
在A窗口设置 隔离级别 read committed; 重复实验一,发现无法脏读
B窗口转账后,提交事务
A窗口查询到 B提交的数据 (不可重复读)
实验三:演示阻止不可重复读发生
在A窗口 设置隔离级别 repeatable read 重复试验二,发现不会发生不可重复读
实验四:演示serializable 串行事务效果
A窗口设置隔离级别 serializable
A、B同时开启事务
B窗口插入一条数据 insert into account values(null,'ddd',1000);
在A窗口查询数据 select * from account;
发现A 窗口阻塞了,等待B事务执行结束
安全性:serializable > repeatable read > read committed > read uncommitted
性能 :serializable < repeatable read < read committed < read uncommitted
结论: 实际开发中,通常不会选择 serializable 和 read uncommitted , mysql默认隔离级别 repeatable read ,oracle默认隔离级别 read committed
JDBC程序中能否指定事务的隔离级别 ?
Connection接口中定义事务隔离级别四个常量:
static int TRANSACTION_READ_COMMITTED
指示不可以发生脏读的常量;不可重复读和虚读可以发生。
static int TRANSACTION_READ_UNCOMMITTED
指示可以发生脏读 (dirty read)、不可重复读和虚读 (phantom read) 的常量。
static int TRANSACTION_REPEATABLE_READ
指示不可以发生脏读和不可重复读的常量;虚读可以发生。
static int TRANSACTION_SERIALIZABLE
指示不可以发生脏读、不可重复读和虚读的常量。
通过 void setTransactionIsolation(int level) 设置数据库隔离级别
两个或多个事务更新同一行,但这些事务彼此之间都不知道其它事务进行的修改,因此第二个更改覆盖了第一个修改
丢失更新问题的解决
悲观锁( Pessimistic Locking )
select * from table lock in share mode(读锁、共享锁)
select * from table for update (写锁、排它锁)
乐观锁( Optimistic Locking )
通过时间戳字段
解决方案详解:
方案一:悲观锁 (假设丢失更新一定会发生 ) ----- 利用数据库内部锁机制,管理事务
方案二:乐观锁 (假设丢失更新不会发生)------- 采用程序中添加版本字段解决丢失更新问题
一、悲观锁
mysql数据库内部提供两种常用 锁机制:共享锁(读锁)和排它锁(写锁)
允许一张数据表中数据记录,添加多个共享锁,添加共享锁记录,对于其他事务可读不可写的
一张数据表中数据记录,只能添加一个排它锁,在添加排它锁的数据 不能再添加其他共享锁和排它锁的 ,
对于其他事物可读不可写的
*** 所有数据记录修改操作,自动为数据添加排它锁
添加共享锁方式:select * from account lock in share mode ;
添加排它锁方式:select * from account for update;
* 锁必须在事务中添加 ,如果事务结束了 锁就释放了
解决丢失更新:事务在修改记录过程中,锁定记录,别的事务无法并发修改
二、乐观锁
采用记录的版本字段,来判断记录是否修改过 -------------- timestamp
timestamp 可以自动更新
create table product (
id int,
name varchar(20),
updatetime timestamp
);
insert into product values(1,'冰箱',null);
update product set name='洗衣机' where id = 1;
* timestamp 在插入和修改时 都会自动更新为当前时间
解决丢失更新:在数据表添加版本字段,每次修改过记录后,版本字段都会更新,如果读取是版本字段,与修改时版本字段不一致,说明别人进行修改过数据 (重改)
事务案例:银行转账(在 service 层处理事务 ThreadLocal)
一次性创建多个连接,将多个连接缓存在内存中 ,形成数据库连接池(内存数据库连接集合),如果应用程序需要操作数据库,只需要从连接池中获取一个连接,使用后,并不需要关闭连接,只需要将连接放回到连接池中。
* 好处:节省创建连接与释放连接 性能消耗 ---- 连接池中连接起到复用的作用 ,提供程序性能
编写连接池需实现javax.sql.DataSource接口。DataSource接口中定义了两个重载的getConnection方法:
Connection getConnection()
Connection getConnection(String username, String password)
实现DataSource接口,并实现连接池功能的步骤:
在DataSource构造函数中批量创建与数据库的连接,并把创建的连接保存到一个集合对象中
实现getConnection方法,让getConnection方法每次调用时,从集合对象中取一个Connection返回给用户。
当用户使用完Connection,调用Connection.close()方法时,Connection对象应保证将自己返回到连接池的集合对象中,而不要把conn还给数据库。
自定义连接池程序问题:
1、尽量不要使用具体对象类型的引用
MyDataSource dataSource = new MyDataSource(); 应该写为 DataSource dataSource = new MyDataSource();
2、使用自定义方法 close 将连接放回连接池 ,需要用户在使用时需要记忆额外API
解决:让用户定义连接池时 DataSource dataSource = new MyDataSource(); 在用户使用连接后,应该调用conn.close(); 完成将连接放回到连接池
对close方法进行方法增强
练习:增强close方法,不真正关闭连接,而是将连接放回到连接池
增强java中一个方法存在三种方式:1、继承 方法覆盖 2、包装 3、动态代理
1、继承方法覆盖,对原有类方法进行加强
注意:必须控制对象构造,才能使用继承的方式对方法进行加强
2、包装 (装饰者)
编写包装类 1) 必须要与被装饰对象,有相同父类或者实现相同接口 2) 必须将被装饰对象传递给装饰者 (构造函数)
包装是一种通用方法加强措施,不管是否控制目标对象创建,都可以完成方法加强
3、动态代理
原理:利用类加载器 ,在内存中根据目标类 加载器和接口 创建一个代理对象 ,通过代理对象完成对原有对象方法进行加强
注意:被代理对象,必须实现接口
应用场景:也是只有存在目标对象,就可以通过动态代理进行加强
DBCP 是Apache 的commons 项目的一个子项目
去官网下载 dbcp 和 pool 的jar包 ,DBCP 依赖 POOL的jar包
复制两个jar 到WEB-INF/lib下
DBCP连接池 核心类 BasicDataSource
* 任何数据库连接池,需要数据库连接,必须通过JDBC四个基本参数构造
1、手动设置参数
// 使用连接池
BasicDataSource basicDataSource = new BasicDataSource();
// 设置JDBC四个基本参数
basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
basicDataSource.setUrl("jdbc:mysql:///day14");
basicDataSource.setUsername("root");
basicDataSource.setPassword("abc");
2、通过配置文件
// 根据属性参数 获得连接池
InputStream in = DBCPTest.class.getResourceAsStream("/dbcp.properties");
Properties properties = new Properties();
// 装载输入流
properties.load(in);
DataSource dataSource = BasicDataSourceFactory.createDataSource(properties)
主流开源连接池,在Hibernate和Spring 都提供对C3P0连接池支持
去下载c3p0 开发包 http://sourceforge.net
将c3p0的jar 复制WEB-INF/lib下
1、手动
// 核心连接池类
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 设置四个JDBC基本连接属性
comboPooledDataSource.setDriverClass("com.mysql.jdbc.Driver");
comboPooledDataSource.setJdbcUrl("jdbc:mysql:///day14");
comboPooledDataSource.setUser("root");
comboPooledDataSource.setPassword("abc");
2、在src下新建c3p0-config.xml
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); 会自定加载配置文件
常用基本连接池属性
acquireIncrement 如果连接池中连接都被使用了,一次性增长3个新的连接
initialPoolSize 连接池中初始化连接数量 默认:3
maxPoolSize 最大连接池中连接数量 默认:15连接
maxIdleTime 如果连接长时间没有时间,将被回收 默认:0 连接永不过期
minPoolSize 连接池中最小连接数量 默认:3
tomcat服务器内置连接池 (使用Apache DBCP)
配置tomcat 内置连接池,通过JNDI方式 去访问tomcat的内置连接池
JNDI Java命名和目录接口,是JavaEE一项技术 ,允许将一个Java对象绑定到一个JNDI容器(tomcat)中 ,并且为对象指定一个名称
通过javax.naming 包 Context 对JNDI 容器中绑定的对象进行查找,通过指定名称找到绑定Java对象
操作步骤
1、配置使用tomcat 内置连接池 配置
context元素有三种常见配置位置
1) tomcat/conf/context.xml 所有虚拟主机,所有工程都可以访问该连接池
2) tomcat/conf/Catalina/localhost/context.xml 当前虚拟主机(localhost)下所有工程都可以使用该连接池
3) 当前工程/META-INF/context.xml 只有当前工程可以访问该连接池
type="javax.sql.DataSource" username="root" password="abc" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql:///day14" maxActive="8" maxIdle="4"/> * 必须先将mysql驱动jar包 复制tomcat/lib下 * 在tomcat启动服务器时,创建连接池对象,绑定 jdbc/EmployeeDB 指定名称上 2、通过运行在JNDI容器内部的程序(Servlet/JSP)去访问tomcat内置连接池 Context context = new InitialContext(); Context envCtx = (Context)context.lookup("java:comp/env"); 固定路径 DataSource datasource = (DataSource) envCtx.lookup("jdbc/EmployeeDB"); 通过绑定名称,查找指定java对象