【web第二十天】事务
事务
1.事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。
例如:A——B转帐,对应于如下两条sql语句
update account set money=money-100 where name=‘a’;
update account set money=money+100 where name=‘b’;
在这个例子中,我们要保证这两条sql要么一起成功,要么一起失败,不允许一部分成功一部分失败,这就要靠数据库的事务来实现了。
2.管理事务
2.1.数据库默认的事务
数据库默认支持事务的,但是数据库默认的事务是一条sql语句独占一个事务,这种模式意义不大。
2.2.手动控制事务
2.2.1.sql控制事务
start transaction;
开启事务,在这条语句之后的所有的sql将处在同一事务中,要么同时完成要么同时不完成。
事务中的sql在执行时,并没有真正修改数据库中的数据。
commit;
提交事务,将整个事务对数据库的影响一起发生。
rollback;
回滚事务,将这个事务对数据库的影响取消掉。
2.2.2.JDBC中控制事务
当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:
conn.setAutoCommit(false);
关闭自动连接后,conn将不会帮我们提交事务,在这个连接上执行的所有sql语句将处在同一事务中,需要我们是手动的进行提交或回滚
conn.commit();
提交事务
conn.rollback();
回滚事务
也可以设置回滚点回滚部分事务。
SavePoint sp = conn.setSavePoint();
conn.rollback(sp);
注意:回到回滚点后,回滚点之前的代码虽然没被回滚但是也没提交呢,如果想起作用还要做commit操作。
3.事务的四大特性
事务的四大特性是事务本身具有的特点。简称ACID。
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
4.隔离性
4.1.数据库隔离性分析
数据库的其他三大特性数据库可以帮我们保证,而隔离性我们需要再讨论。
我们知道数据库的隔离性问题本质上就是多线程并发安全性问题。
可以用锁来解决多线成并发安全问题,但是如果用了锁,必然会造成程序的性能大大的下降.对于数据库这种高并发要求的程序来说这是不可接受的.
我们可以具体分析下隔离性产生的细节:
如果两个线程并发修改,必然产生多线程并发安全问题,必须隔离开
如果两个线程并发查询,必然没有问题,不需要隔离
如果一个线程修改,一个线程查询,在不同的应用场景下有可能有问题,有可能没问题。
4.2.隔离性可能造成的问题
4.2.1.脏读:
打开两个mysql客户端,都执行以下语句。
set session transaction isolation level read uncommitted;
一个事务读取到另一个事务未提交的数据:a买鞋,b卖鞋
----------------------------
a 1000
b 1000
----------------------------
客户端a:
start transaction;
update account set money = money-100 where name='a';
update account set money = money+100 where name='b';
-----------------------------
客户端b:
start transaction;
select * from account;
a 900
b 1100
commit;
-----------------------------
客户端a:
rollback;
-----------------------------
客户端b:
start transaction;
select * from account;
a 1000
b 1000
commit;
-----------------------------
4.2.2.不可重复读:
一个事务多次读取数据库中的同一条记录,多次查询的结果不同(一个事务读取到另一个事务已经提交的数据) a:银行账户,W:银行工作人员(领导让W统计a的账户情况)
------------------------------
活期 定期 固定资产
a 1000 1000 1000
------------------------------
W:
start transaction;
select 活期 from account where name='a'; -- 活期存款:1000W元
select 定期 from account where name = 'a'; -- 定期存款:1000W元
select 固定 from account where name = 'a'; -- 固定资产:1000W元
---------------------------
a:
start transaction;
update account set 活期=活期-1000 where name= 'a';
commit;
---------------------------
select 活期+定期+固定 from account where name='a'; ---总资产:2000W元
4.2.3.虚读(幻读)
有可能出现,有可能不出现,概率非常低:一个事务多次查询整表数据,多次查询时,由于有其他事务增删数据, 造成的查询结果不同(一个事务读取到另一个事务已经提交的数据)
------------------------------
a 1000
b 2000
------------------------------
工作人员d:
start transaction;
select sum(money) from account; --- 总存款3000元
select count(*) from account; --- 总账户数2个
-----------------
c:
start transaction;
insert into account values (null,'c',3000);
commit;
-----------------
select avg(mone) from account; --- 平均每个账户:2000元
5.数据库的隔离级别
数据库设计者在设计数据库时到底该防止哪些问题呢?防止的问题越多性能越低,防止的问题越少,则安全性越差。
到底该防止哪些问题应该由数据库使用者根据具体的业务场景来决定,所以数据库的设计者并没有把放置哪类问题写死,而是提供了如下选项:
数据库的四大隔离级别:
read uncommitted;
不做任何隔离,可能造成脏读、不可重复度、虚读(幻读)问题
read committed;
可以防止脏读,但是不能防止不可重复度、虚读(幻读)问题
repeatable Read;
可以防止脏读、不可重复度,但是不能防止虚读(幻读)问题
serializable;
可以防止所有隔离性的问题,但是数据库就被设计为了串行化的数据库,性能很低
从安全性上考虑:
Serializable > Repeatable Read > Read Committed > Read uncommitted
从性能上考虑:
Read uncommitted > Read committed > Repeatable Read > Serializable
我们作为数据库的使用者,综合考虑安全性和性能,从四大隔离级别中选择一个在可以防止想要防止的问题的隔离级别中性能最高的一个。
其中Serializable性能太低用的不多,Read uncommitted安全性太低用的也不多,我们通常从Repeatable Read和Read committed中选择一个。
如果需要防止不可重复读选择Repeatable Read,如果不需要防止选择Read committed
mysql数据库默认的隔离级别就是Repeatable Read
Oracle数据库默认的隔离级别是Read committed
6.操作数据库的隔离级别
查询数据库的隔离级别:
select @@tx_isolation;
修改数据库的隔离级别:
set [session/global] transaction isolation level xxxxxx;
不写默认就是session,修改的是当前客户端和服务器交互时是使用的隔离级别,并不会影响其他客户端的隔离级别
如果写成global,修改的是数据库默认的隔离级别(即新开客户端时,默认的隔离级别),并不会修改当前客户端和已经开启的客户端的隔离级别
set global transaction isolation level serializable;
7.数据库中的锁
7.1.共享锁
共享锁和共享锁可以共存,共享锁和排他锁不能共存.在非Serializable隔离级别下做查询不加任何锁,在Serializable隔离级别下做查询加共享锁。
案例演示:打开两个mysql客户端,将隔离级别都设置为Serializable级别,
set session transaction isolation level Serializable;--设置后查询加了共享锁
分别在两个客户端中查询:
start transaction;
select * from account;--都能查询出数据,说明共享锁可以共存。
7.2.排他锁
排他锁和共享锁不能共存,排他锁和排他锁也不能共存,在任何隔离级别下做增删改都加排他锁。
在7.1的基础上,在其中一个客户端执行修改操作,将一个客户端的共享锁升级为排他锁:
两个客户端都执行:
start transaction;
select * from account;
-----------------
一个客户端执行:
update account set money = 900;-- #发现执行在等待,当另外一个客户端提交commit或者回滚rollback之后,修改才能成功。
另外一个客户端执行:
rollback/commit;
7.3.死锁
mysql可以自动检测到死锁,错误退出一方并执行另一方。
在7.1基础上:
两个客户端都执行:
start transaction;
select * from account;
-----------------
一个客户端执行:
update account set money = 900;
另外一个客户端执行:
update account set money = 800;
发现彼此等待,直到一方报错结束,死锁才结束。
8.EasyMall添加商品 - 事务控制
事务应该加在业务层还是dao层?
因为dao层的一个方法就是一个C/R/U/D其中的一个操作,所以需要将事务的代码写在业务层的实现类中。
但是业务层获取的conn,和dao层的conn是各自的数据库连接,他们并没有共享在业务层获取的数据库连接,所以在调用方法时,应该将数据库连接对象作为参数传递过去。
EasyMall添加商品事务控制代码示例:(版本一)
在ProdServiceImpl中添加事务控制代码,并将此connection通过参数传递到dao层,添加或修改如下代码: private ProdDao dao = BasicFactory.getFactory().getInstance(ProdDao.class);
public void addProd(Prod prod) throws MsgException { Connection conn = null; try { //开启事务 conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); //1.根据商品种类名称查询商品种类表 String cname = prod.getCname(); ProdCategory findPc = dao.findProdCategoryByCname(conn,cname);
//2.处理商品种类 int cid = 0; if(findPc == null){ //如果找不到,则向商品种类表中加入新的商品种类 ProdCategory pc = new ProdCategory(); pc.setCname(cname); dao.addProdCategory(pc); //并获取新增加的商品的id 作为外键保存在商品信息里 ProdCategory findPc2 = dao.findProdCategoryByCname(conn,cname); cid = findPc2.getId(); }else{ //如果查找到,则使用该商品种类的id作为外键保存在商品信息里 cid = findPc.getId(); }
//3.将商品信息加入商品表 prod.setCid(cid); dao.addProd(conn,prod); //提交事务 conn.commit(); } catch (SQLException e) { //回滚事务 try { if(conn != null){ conn.rollback(); } } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); throw new RuntimeException(); } }
|
Dao层代码也需要为之改变(不做演示)。 |
此代码虽然可以解决事务控制问题,但是代码耦合性大
Service要控制事务,事务需要Connection对象,Connection对象是Dao层特有的对象,Service不应该持有Connection。
Service不能用Connection,又要控制事务,矛盾了,怎么办?
有些耦合性无法避免 --> 想办法管理起来 --> 将所有控制事务的代码整合到一个工具类中来进行管理
开发TransactionManager,在其中管理Connection,并对外提供getConn、startTran、commitTran、rollbackTran、release方法。
之后所有和事务 相关的操作都不要直接使用Conn 而是通过TransactionManager来实现管理
解决耦合性的问题 - 本质上是将耦合转移到了TransactionManager中同一管理。
虽然没有彻底的解决耦合,但是统一管理起来,方便未来开发和维护。
EasyMall添加商品事务控制代码示例:(版本二)
在utils包中创建类:TransactionManager,并添加如下代码: private TransactionManager(){ } private static Connection conn = null; static{ conn = JDBCUtils.getConnection(); } /**获取数据库连接的方法 * @return 数据库连接对象 */ public static Connection getConn(){ return conn; } /**开启事务 */ public static void startTran(){ try { conn.setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } /**提交事务 */ public static void commitTran(){ try { conn.commit(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } /**回滚事务 */ public static void rollbackTran(){ try { conn.rollback(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } /**释放资源 */ public static void release(){ JDBCUtils.close(conn, null, null); }
|
在ProdServiceImpl中修改代码如下: private ProdDao dao = BasicFactory.getFactory().getInstance(ProdDao.class);
public void addProd(Prod prod) throws MsgException { try { //开启事务 TransactionManager.startTran(); //1.根据商品种类名称查询商品种类表 String cname = prod.getCname(); ProdCategory findPc = dao.findProdCategoryByCname(cname); //2.处理商品种类 int cid = 0; if(findPc == null){ //如果找不到,则向商品种类表中加入新的商品种类 ProdCategory pc = new ProdCategory(); pc.setCname(cname); dao.addProdCategory(pc); //并获取新增加的商品的id 作为外键保存在商品信息里 ProdCategory findPc2 = dao.findProdCategoryByCname(cname); cid = findPc2.getId(); }else{ //如果查找到,则使用该商品种类的id作为外键保存在商品信息里 cid = findPc.getId(); } //3.将商品信息加入商品表 prod.setCid(cid); dao.addProd(prod); //提交事务 TransactionManager.commitTran(); } catch (Exception e) { //回滚事务 TransactionManager.rollbackTran(); e.printStackTrace(); throw new RuntimeException(); }finally{ //关闭数据库链接 TransactionManager.release(); } }
|
在ProdDaoImpl中,将获取conn的代码从JDBCUtils.getConnection()改为从TransactionManager中获取,例如: conn = TransactionManager.getConn();
将所有关闭数据库链接的操作中,不要将链接conn关闭,代码改为: } finally { JDBCUtils.close(null, ps, rs); } |
测试发现,当添加完商品再次添加后,就会抛出异常:
SQLException: You can't operate on a closed Connection!!!
由于第三方管理类中,连接定义成了一个静态的,整个项目中共用一个数据库连接对象,添加完订单之后,关闭了该连接对象,再次查询全部商品时,使用的连接对象是一个已经关闭的数据库连接对象,所以抛出以上异常。
经过分析要想解决问题,需要让每一个服务的线程中保存一个独立数据库连接对象。
9.ThreadLocal:本地线程变量(重要!)
9.1.ThreadLocal概述
在线程内部保存数据,利用线程对象在线程执行的过程中传递数据,另外由于每个线程各自保存各自的数据conn,所以可以避免线程并发安全问题。
Thread对象内置了一个Map来存取消息,但是这个map外界无法直接操作,需要通过ThreadLocal来实现对Thread中的Map进行数据的存取。
9.2.ThreadLocal方法
ThreadLocal tl = new ThreadLocal();
tl.set(obj); --向当前线程中的map保存对象,key为当前线程变量,值为存入的数据。
tl.get(); --从当前线程中获取对象,如果获取不到对象,调用initialValue()创建一个新的对象。
tl.remove(); --从当前线程中删除对象。
initialValue() --创建对象。
EasyMall添加商品事务控制代码示例:(版本三:最终)
在TransactionManager中,加入ThreadLocal代码,通过本地线程变量获取conn,代码如下: private TransactionManager(){ }
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>(){ protected Connection initialValue() { return JDBCUtils.getConnection(); }; };
/**获取数据库连接的方法 * @return 数据库连接对象 */ public static Connection getConn(){ return tl.get(); } /**开启事务 */ public static void startTran(){ try { tl.get().setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } /**提交事务 */ public static void commitTran(){ try { tl.get().commit(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } /**回滚事务 */ public static void rollbackTran(){ try { tl.get().rollback(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } /**释放资源 */ public static void release(){ JDBCUtils.close(tl.get(), null, null); //从本地线程变量中删除 tl.remove(); } |