1. 事务
1.1. 事务的概念
1.1.1. 事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。
例如:A——B转帐,对应于如下两条sql语句
update account set money=money-100 where name=‘a’;
update account set money=money+100 where name=‘b’;
1.2. 管理事务
1.2.1. 数据库默认的事务
数据库默认支持事务的,但是数据库默认的事务是一条sql语句独占一个事务,这种模式,意义不大.
1.2.2. 手动控制事务!!
如果希望自己控制事务也是可以的:
start transaction;
-- 开启事务,在这条语句之后的所有的sql将处在同一事务中,要么同时完成要么同时不完成
--事务中的sql在执行时,并没有真正修改数据库中的数据
commit;
-- 提交事务,将整个事务对数据库的影响一起发生
rollback;
-- 回滚事务,将这个事务对数据库的影响取消掉
1.2.3. JDBC中控制事务!!!
当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:
conn.setAutoCommit(false);
--关闭自动连接后,conn将不会帮我们提交事务,在这个连接上执行的所有sql语句将处在同一事务中,需要我们是手动的进行提交或回滚
conn.commit();
--提交事务
conn.rollback();
--回滚事务
也可以设置回滚点回滚部分事务。
Savepoint sp = conn.setSavepoint();
conn.rollback(sp);
--注意,回到回滚点后,回滚点之前的代码虽然没被回滚但是也没提交呢,如果想起作用还要做commit操作.
案例演示:
create database day20;
use day20;
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
insert into account(name,money) values ('a',1000.0);
insert into account(name,money) values ('b',1000.0);
演示案例:
public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; Savepoint sp = null;
try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql:///day23","root","root"); //开启事务 conn.setAutoCommit(false); //a-100 ps = conn.prepareStatement("update account set money = money - ? where name = ?"); ps.setDouble(1, 100); ps.setString(2, "a"); ps.executeUpdate(); //b+100 ps = conn.prepareStatement("update account set money = money + ? where name = ?"); ps.setDouble(1, 100); ps.setString(2, "b"); ps.executeUpdate(); //设置保存点 sp = conn.setSavepoint(); //b-200 ps = conn.prepareStatement("update account set money = money - ? where name = ?"); ps.setDouble(1, 200); ps.setString(2, "b"); ps.executeUpdate(); //人为的制造异常 int i = 1/0; //a+200 ps = conn.prepareStatement("update account set money = money + ? where name = ?"); ps.setDouble(1, 200); ps.setString(2, "a"); ps.executeUpdate();
//提交事务 conn.commit(); } catch (Exception e) { try { if(conn!=null) if(sp==null){ conn.rollback(); }else{ conn.rollback(sp); conn.commit(); } } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }
} |
1.3. 事务的四大特性
1.3.1. 事务的四大特性
拷贝笔记
1.4. 隔离性
1.4.1. 数据库隔离性分析
拷贝
1.5. 隔离性可能造成的问题
1.5.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;
-----------------------------
1.5.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元
1.5.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 ('c',3000);
commit;
-----------------
select avg(mone) from account; --- 平均每个账户:2000元
1.6. 数据库的隔离级别
1.6.1. 四大隔离级别
那么数据库设计者在设计数据库时到底该防止哪些问题呢?防止的问题越多性能越低,防止的问题越少,则安全性越差。
到底该防止哪些问题应该由数据库使用者根据具体的业务场景来决定,所以数据库的设计者并没有把放置哪类问题写死,而是提供了如下选项:
数据库的四大隔离级别:
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,修改的是数据库默认的隔离级别(即新开客户端时,默认的隔离级别),并不会修改当前客户端和已经开启的客户端的隔离级别
set global transaction isolation level serializable;
1.8. 数据库中的锁:
1.8.1. 共享锁
共享锁和共享锁可以共存,共享锁和排他锁不能共存.在非Serializable隔离级别下做查询不加任何锁,在Serializable隔离级别下做查询加共享锁.
案例演示:打开两个mysql客户端,将隔离级别都设置为Serializable级别,
set session transaction isolation level Serializable;--设置后查询加了共享锁
分别在两个客户端中查询:
start transaction;
select * from account;--都能查询出数据,说明共享锁可以共存。
1.8.2. 排他锁
排他锁和共享锁不能共存,排他锁和排他锁也不能共存,在任何隔离级别下做增删改都加排他锁.
在1.8.1的基础上,在其中一个客户端执行修改操作,将一个客户端的共享锁升级为排他锁:
两个客户端都执行:
start transaction;
select * from account;
-----------------
一个客户端执行:
update account set money = 900;-- #发现执行在等待,当另外一个客户端提交commit或者回滚rollback之后,修改才能成功。
另外一个客户端执行:
rollback/commit;
1.8.3. 可能的死锁
mysql可以自动检测到死锁,错误退出一方执行另一方
在1.8.1基础上:
两个客户端都执行:
start transaction;
select * from account;
-----------------
一个客户端执行:
update account set money = 900;
另外一个客户端执行:
update account set money = 800;
发现彼此等待,直到一方报错结束,死锁才结束。
1.9. 更新丢失问题
1.9.1. 更新丢失问题的产生
- 游戏平台开发,靠充值挣钱,支付模块--:
两个并发的事务基于同一个查询结果进行修改,后提交的事务忽略了先提交的事务对数据库的影响,造成了先提交的事务对数据库的影响丢失,这个过程就叫做更新丢失
1.9.2. 更新丢失解决方案:
将数据库设置为Serializable隔离级别,可以直接避免该问题的发生。但是我们一般不会将数据库设置为Serializable。
那么在非Serializable下又如何解决更新丢失?可以使用乐观锁、悲观锁。
乐观锁和悲观锁并不是数据库中真实存在的锁,而是两种解决方案的名字。
(1)悲观锁:悲观的认为每一次修改,都会造成更新丢失。
在查询时,手动的加排他锁,从而在查询时就排除可能的更新丢失。
select * from users for update;
(2)乐观锁:
在表中设计版本字段,在进行修改时,要求根据具体版本进行修改,并将版本字段+1,如果更新失败,说明更新丢失,需要重新进行更新。
总结:两种解决方案各有优缺点,如果查询多修改少,用乐观锁.如果修改多查询,用悲观锁。
2. 订单模块开发
从购物车生成订单,查询自己的订单,删除订单等功能。
1完成订单添加
开发思路:先编写addOrder.jsp->在思考需要用到什么方法OrderAddServlet
1、 首先将订单添加页面创建addOrder.jsp(参考购物车cart.jsp),主要用到了map集合的迭代输出。
<link href="${app }/css/addOrder.css" rel="stylesheet" type="text/css"> </head> <body> <%@ include file="/_head.jsp" %> <div class="warp"> <form name="form1" method="post" action="${app}/servlet/OrderAddServlet"> <h3>增加订单</h3> <div id="forminfo"> <span class="lf">收货地址:</span> <label for="textarea"></label> <textarea name="receiverinfo" id="textarea" cols="45" rows="5"></textarea> <br> 支付方式:<input name="" type="radio" value="" checked="checked"> 在线支付 </div> <table width="1024" height="80" border="1" cellpadding="0" cellspacing="0" bordercolor="#d8d8d8"> <tr> <th width="276">商品图片</th> <th width="247">商品名称</th> <th width="231">商品单价</th> <th width="214">购买数量</th> <th width="232">总价</th> </tr> <c:set var="money" value="0"></c:set> <c:forEach items="${sessionScope.cart }" var="entry"> <tr> <td>${entry.key.name }</td> <td>${entry.key.category }</td> <td>${entry.key.price }元</td> <td>${entry.value }件</td> <td>${entry.key.price * entry.value }元</td> </tr> <c:set var="money" value="${money+(entry.key.price * entry.value)}"></c:set> </c:forEach> </table> <div class="Order_price">总价:${money }元</div> <div class="add_orderbox"> <input name="" type="submit" value="增加订单" class="add_order_but"> </div> </form> </div> <%@ include file="/_foot.jsp" %> |
2、OrderAddServlet逻辑分析
2.1、首先看用户是否登录,没有登录直接跳转到登录页面,添加订单需要用到用户的id
2.2、从session中获取购物车中商品的信息,使用这些信息封装一个Order对象,和N个OrderItem对象。由于OrderItem需要用到订单的id,所以在循环创建OrderItem之前,首先要使用UUID产生订单的id。(该步骤需要用到两个JavaBean:Order和OrderItem)
public class Order { private String id; private double money; private String receiverinfo; private int paystate; private Timestamp ordertime; private int user_id; ..... } |
public class OrderItem { private String order_id; private String product_id; private int buynum; ... } |
public class AddOrderServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User user = (User) request.getSession().getAttribute("user"); //1.将订单信息封装到bean中 Order order = new Order(); order.setId(UUID.randomUUID().toString()); order.setPaystate(0); order.setOrdertime(new Timestamp(new Date().getTime())); order.setReceiverinfo(request.getParameter("receiverinfo")); order.setUser_id(user.getId()); Map<Product,Integer> cart = (Map<Product, Integer>) request.getSession().getAttribute("cart");
//循环遍历购物车,计算订单金额,所有创建订单项对象并存储到集合 List<OrderItem> list = new ArrayList<OrderItem>(); double money = 0; for(Map.Entry<Product, Integer> entry : cart.entrySet()){ money += entry.getKey().getPrice() * entry.getValue();
OrderItem item = new OrderItem(); item.setOrder_id(order.getId()); item.setProduct_id(entry.getKey().getId()); item.setBuynum(entry.getValue()); list.add(item); } order.setMoney(money);
//2.调用Service增加订单 OrderService service = BasicFactory.getFactory().getInstance(OrderService.class); service.addOrder(order,list);
//3.清空购物车 cart.clear();
//4.提示增加订单成功回到主页 response.getWriter().write("增加订单成功!"); response.setHeader("Refresh", "1;url="+request.getContextPath()+"/index.jsp"); }} |
2.3、当订单和订单项创建完毕后,就有了一个Order order 和一个List<OrderItem> list,然后我们需要设计一个Service方法:addOrder(Order order,List<OrderItem> list);
public interface OrderService{ /** * 增加订单 * @param order 封装了订单信息的bean * @param list 存储了当前订单相关的所有订单项对象的集合 */ void addOrder(Order order, List<OrderItem> list); } |
3、分析OrderServiceImpl,在实现类一定会调用到OrderDao(由于订单项不能脱离于订单直接添加,所有我将订单项的CRD放到了OrderDao中) ,另外由于订单添加需要使用事务,所以我们还得在addOrder中获取数据库连接
private OrderDao order_dao = BasicFactory.getFactory(). getInstance(OrderDao.class); private ProductDao prod_dao = BasicFactory.getFactory(). getInstance(ProductDao.class);
public void addOrder(Order order, List<OrderItem> list) { Connection conn = null; try { conn = DaoUtils.getConn(); conn.setAutoCommit(false); //1.增加订单 order_dao.addOrder(conn,order); //2.检查库存数量,如果充足,扣除库存 并增加订单项 for(OrderItem item : list){ Product prod = prod_dao.findProdById (conn,item.getProduct_id()); if(prod.getPnum() >= item.getBuynum()){ //扣除库存数量 prod_dao.updateProd(conn,prod.getId(), prod.getPnum() - item.getBuynum()); //库存充足可以购买 order_dao.addOrderItem(conn,item); }else{ //库存不足 throw new MsgException("商品库存不足!"+prod.getId()+"-"+prod.getName()); } } //没有出现异常提交事务 conn.commit(); }catch(MsgException me){ throw me; }catch (Exception e) { //出现异常回滚事务 try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); throw new RuntimeException(e); } finally{ DaoUtils.close(conn, null, null); } } |
4、创建OrderDao和OrderDaoImpl以及添加对应的方法,ProductDao和ProductDaoImpl添加方法
public interface OrderDao{ /** * 增加订单表记录 * @param order */ void addOrder(Connection conn,Order order);
/** * 增加订单项记录 * @param item 订单项信息bean */ void addOrderItem(Connection conn,OrderItem item); } |
public class OrderDaoImpl implements OrderDao { public void addOrder(Connection conn, Order order) { String sql = "insert into orders(id,money,receiverinfo, paystate,ordertime,user_id) values (?,?,?,?,?,?)"; DaoUtils.update(conn,sql, order.getId(),order.getMoney(), order.getReceiverinfo(),order.getPaystate(),order.getOrdertime(), order.getUser_id());//在DaoUtils中添加一个新的方法update方法 }
public void addOrderItem(Connection conn, OrderItem item) { String sql = "insert into orderitem(order_id,product_id,buynum) values (?,?,?)"; DaoUtils.update(conn,sql, item.getOrder_id(),item.getProduct_id(),item.getBuynum());
} |
public void updateProd(Connection conn, String id, int newNum) { String sql= "update products set pnum=? where id=?"; DaoUtils.update(conn,sql, newNum,id); } |
5、在DaoUtils添加一个新的方法:
public static int update(Connection conn, String sql, Object ...args ) { PreparedStatement ps = null; try{ ps = conn.prepareStatement(sql); for(int i = 0;i<args.length;i++){ ps.setObject(i+1, args[i]); } return ps.executeUpdate(); }catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); }finally{ DaoUtils.close(null, ps, null);//数据库连接千万不要关闭 } } |
6、config.properties文件添加
OrderDao=cn.tedu.dao.OrderDaoImpl OrderService =cn.tedu.service.OrderServiceImpl |
这么做可以解决事务的问题,但是在Service层操作Dao层的数据库链接对象,耦合性太高,
如何解决?
数据库连接借给第三方管理。
2事务解耦合
1、 创建TransactionManager
public class TransactionManager { private static Connection conn; static{ try { conn= DaoUtils.getConn(); } catch (SQLException e) { e.printStackTrace(); } } private TransactionManager(){ } public static Connection getConn(){ return conn; } public static void startTran(){ try { conn.setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } public static void commitTran(){ try { conn.commit(); } catch (SQLException e) { e.printStackTrace(); } } public static void rollbackTran(){ try { conn.rollback(); } catch (SQLException e) { e.printStackTrace(); } } public static void release(){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } |
2、 将OrderDao、OrderDaoImpl、ProductDao、ProductDao中涉及到的方法不传递conn,用到时而是从TransactionManager中获取。
3、 修改后添加订单测试,第一次没有问题,但是第二次添加订单时,发现出现连接已经关闭的异常。从而引出ThreadLocal
3ThreadLocal:本地线程变量(重点)
在线程内部保存数据,利用线程对象在线程执行的过程中传递数据,另外由于每个线程各自保存各自的数据conn,所以可以避免线程并发安全问题。
ThreadLocal -- 本地线程变量
是一种参数传递的机制
set(Object obj):向当前线程保存对象
Object get():从当前线程中获取对应,如果获取不到对象,调用initialValue()创建一个新的对象。
remove():从当前线程中删除对象
initialValue():创建对象。
package cn.tedu.utils; import java.sql.Connection; import java.sql.SQLException;
public class TransactionManager { private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>(){ protected Connection initialValue(){ try { return DaoUtils.getConn(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } }; private TransactionManager(){ } public static Connection getConn(){ return tl.get(); } public static void startTran(){ try { tl.get().setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } public static void commitTran(){ try { tl.get().commit(); } catch (SQLException e) { e.printStackTrace(); } } public static void rollbackTran(){ try { tl.get().rollback(); } catch (SQLException e) { e.printStackTrace(); } } public static void release(){ try { tl.get().close(); tl.remove(); } catch (SQLException e) { e.printStackTrace(); } } } |
总结:由于每个h线程有有各自的变量(conn)副本,所以可以防止多线程并发安全问题。