一、事务
1.1概是指逻辑上的一组操作,组成这组操作的各个单元,要么全成功(提交),要么全部不成功(回滚到开始处)。
1.2管理事务
1.数据默认支持事务的,但是数据库默认的事务是一条sql语句(独占一个事务),意义不大。
2.手动控制事务(命令行)!!
如果希望自己手动控制事务也是可以的,以下操作:
start transaction;
--开启事务,在这条命令以后的所有sql语句将处在同一个事务中,要么全成功,要么全部成功。
事务中的sql语句在执行时,并没有真正意义的修改数据库的数据。
commit;
--提交事务,将整个事务对数据库的影响一起发生效果。
rollback;
--回滚事务,将这个事务对数据库的影响取消掉。
3.JDBC中控制事务!!!
当JDBC程序向数据库获取到Connection对象时,默认情况下,这个Connection对象会自动提交事务,
向数据库提交在它上面的sql语句。若想关闭默认提交方式,让多条sql语句在同一个事务中执行,可以
使用一下语句:
conn.setAutoCommit(false);
-取消自动提交方式,变为手动提交。
conn.commit();
--提交事务
也可以设置回滚点(还原点),实现回滚部分事务。
Savepoint sp = conn.setSavepoint();
conn.rollback();
--回滚事务,回滚事务开启之前的地方。
conn.rollback(sp);
--回滚事务,回滚到sp(回滚点)。
其他的部分事务,要想生效还需要执行提交:conn.commit();
1.3事务的四大特性!!!
事务的四大特性是事务本身具有的特点。简称ACID。
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,
多个并发事务之间数据要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库
发生故障也不应该对其有任何影响。
1.4、隔离性!!!
数据库的其他三大特性数据库可以帮我们保证,而隔离性我们需要再讨论。
如果我们是数据库的设计者,该如何考虑设计数据库保证数据库的隔离性呢?
我们知道数据库的隔离性问题本质上就是多线程并发安全性问题。
性能和安全
可以用锁来解决多线成并发安全问题,但是如果用了锁,必然会造成程序的性能大大的下降.对于数据库
这种高并发要求的程序来说这是不可接受的.
我们可以具体分析下隔离性产生的细节:
如果两个线程并发修改,必然产生多线程并发安全问题,必须隔离开
如果两个线程并发查询,必然没有问题,不需要隔离,
如果一个线程修改,一个线程查询,在不同的应用场景下有可能有问题,有可能没问题。
1.5.隔离性可能造成的问题
1.5.1. 脏读:
一个事务读取到另外一个事务“未提交”的数据。
//数据还原
update account set money = 1000 where 1=1;
//设置当前会话的隔离级别
set session transaction isolation level read uncommitted;
1.5.2.不可重复读
一个事务多次读取数据库中的同一条记录,多次查询的结果不同(一个事务读取到另外一个已经提交
的数据。)
1.6数据库的隔离级别
那么数据库设计者在设计数据库时到底该防止哪些问题呢?防止的问题越多性能越低,防止的问题越少,
则安全性越差。
到底该防止哪些问题应该由数据库使用者根据具体的业务场景来决定,所以数据库的设计者并没有把
放置哪类问题写死,而是提供了如下选项:
数据库的四大隔离级别:
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
1.7.操作数据库的隔离级别
1.7.1. 查询数据库的隔离级别
select @@tx_isolation;
1.7.2. 修改数据库的隔离级别
set [session/global] transaction isolation level xxxxxx;
不写默认就是session,修改的是当前客户端和服务器交互时是使用的隔离级别,并不会影响其
他客户端的隔离级别
如果写成global,修改的是数据库默认的隔离级别(即新开客户端时,默认的隔离级别),并不会
修改当前客户端和已经开启的客户端的隔离级别
1.8.数据库中的锁:
1.8.1.共享锁
共享锁和共享锁可以共存,共享锁和排他锁不能共存.
在非Serializable隔离级别下做查询不加任何锁,在Serializable隔离级别下做查询加共享锁.
演示:共享锁和共享锁可以共存,共享锁和排他锁不能共存.
分别在两个数据库客户端执行以下命令:
set session transaction isolation level serializable;
start transaction;
select * from account;--在两个事物未提交之前,都可以查询出结果,说明:共享锁和共享可以共存。
在其中一个客户端中执行:
update account set money= 900 where name=‘a‘;
当按下回车键之后,出现执行等待。。。然后在另外一个客户端中执行:
commit;
之后,发现当前客户端的修改操作接着执行完成。
说明:共享锁和排他锁不可共存。
1.8.2.排他锁
排他锁和共享锁不能共存,排他锁和排他锁也不能共存(serializable),在任何隔离级别下做增删改都加排他锁.
1.8.3. 可能的死锁
mysql可以自动检测到死锁,错误退出一方执行另一方
在1.8.1的案例演示基础上(两个客户端的隔离界别都是serializable)
两个客户端:
start transaction;
select * from account;
-----------------
其中一个客户端执行:
update account set money = 800;
另外一个客户端执行:
update account set money = 900;
1.9 更新丢失!!!+
1.9.1概念
两个并发的事务基于同一个查询的结果进行修改,后提交的事务忽略了先提交的事务对数据的影响,造成了
先提交的事务对数据影响的丢失,这个过程被称为更新丢失。
更新丢失问题的产生:
1.游戏平台的开发,靠充值挣钱,支付模块:
分析图见:更新丢失-悲观锁-游戏平台在线支付
1.9.2更新丢失的解决方案!!!
将数据的隔离级别设置为serializable,可以直接避免该问题的发生。但是我们一般不会将数据库的
隔离级别设置serializable。所以该解决方案很少使用。
那么在非serializable隔离级别下时,如何解决更新丢失的问题?可以使用乐观锁和悲观锁。
乐观锁和悲观锁并不是数据库中的真实存在的,而是这两种解决方案的名称。
(1)悲观锁:悲观的认为每一次修改,都会造成更新丢失的问题。
在查询时,手动的加排他锁,从而在查询时就排除可能的更新丢失。
select * from orders where id = 88 for update;
(2)乐观锁
在表设计时,添加一个版本的字段,在进行修改时,要求根据查询出的版本信息进行修改,并将版本字段
+1,如果更新失败,说明更新丢失,需要重新进行更新。
分析图见:更新丢失-乐观锁-东方不败
总结:两种解决方案各有优缺点,如果查询多修改少,用乐观锁;如果修改多查询少,使用悲观锁。
二、升级EasyMall
2.1升级订单添加模块,完成事务版
1修改业务成的订单添加的方法:
public void addOrder(Order order, List<OrderItem> itemList) {
//添加订单
Connection conn = null;
try {
conn = DbUtils.getConn();
conn.setAutoCommit(false);//开启事务
orderDao.addOrder(conn,order);
for (OrderItem item : itemList) {
//查询对应的商品信息
Product prod = prodDao.findProdById(conn,item.getProduct_id());
//检查商品库存是否充足
if(prod.getPnum()>=item.getBuynum()){
//扣掉本次购买
prodDao.updatePnum(conn,item.getProduct_id(),
prod.getPnum()-item.getBuynum());
//向orderitem表中添加一条记录
orderDao.addOrderItem(conn,item);
}else{//库存不足
throw new MsgException("商品库存不足,商品id:"+item.getProduct_id()+",商品名称:"+prod.getName());
}
}
2、在dao层重载用到四个方法(分别在**Dao和**DaoImpl进行重载)
OrderDao和OrderDaoImpl
addOrder(conn,order)
orderDao.addOrderItem(conn,item);
ProductDao和ProductDaoImpl
findProdById(conn,item.getProduct_id())
prodDao.updatePnum(conn,item.getProduct_id(),
3、修改DbUtils类,在该类中重载update和query方法
注意:update和query方法中千万不要关闭数据库连接对象conn
public static <T> T query(Connection conn,String sql, ResultSetHandler<T> rsh,
Object... params){
PreparedStatement ps = null;
ResultSet rs = null;
try {
//获取数据库连接
//预编译sql语句并返回PreparedStatement对象
ps = conn.prepareStatement(sql);
//为占位符赋值
for(int i = 0;i<params.length;i++){//占位符设置下标从1开始
ps.setObject(i+1, params[i]);
}
//执行查询操作
rs = ps.executeQuery();
return rsh.handler(rs);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally{
//关闭数据连接释放资源
close(rs, ps, null);//千万不要关闭数据库连接
}
}
public static int update(Connection conn,String sql, Object... params){
PreparedStatement ps = null;
try {
//获取数据库连接
//预编译sql语句并返回PreparedStatement对象
ps = conn.prepareStatement(sql);
//为占位符赋值
for(int i = 0;i<params.length;i++){
ps.setObject(i+1, params[i]);
}
//执行操作,并返回影响的行数
return ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally{
close(ps,null);//千万不要关闭数据库连接
}
}
2.2.事务的解耦合
归属于dao层的对象conn,目前用在了业务层,如何解耦?
将conn对象交给第三方区管理,业务层将看不到conn对象。
三、ThreadLocal本地线程变量(重点!!!)
在线程的内部保存数据,利用线程对象在线程执行的过程中传递数据(conn对象),另外
由于每一个线程对象保存各自的数据库连接对象conn,所以对其中一个线程中conn对象的close()
不会影响其他线程中的conn对象,也就解决了线程并发安全的问题。
是一种数据传递的机制。
void set(T value):向当前线程中保存对象
T get() 返回本地线程变量中的对象(之前保存的对象)。 如果获取不到对象,则调用initialValue()
创建一个新的对象。
T initialValue() 返回本地线程变量中初始化对象。
void remove() 从本地线程变量中将保存的对象删除。
使用ThreadLocal升级代码
修改TransManager
------------
总结:由于每个线程都有各自的本地线程变量(又保存了自己各自的数据库连接对象conn),所有
可以防止多线程并发安全的问题。
目前,程序还存在以下两个问题:
1、在需要事务的业务层添加事务时,处理事务的代码比较麻烦
2、调用dao层的方式时,使用事务和不使用事务的方法类似,但是我们需要
添加很多类似方法。
如果想解决以上两个问题,需要使用注解和动态代理。
@Tran
预习、注解和动态代理