事务
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功
数据库开启事务命令
- start transaction 开启事务
- Rollback 回滚事务
-
Commit 提交事务
//方式一
//创建表
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);
//创建事务
start transaction;
update account money=money-500 where id=1;
update account money=money+500 where id=1;
update account money=money+500 where id=1;
rollback;//事务回滚,回滚到最开始的样子
commit;//只有commit后,以上才会操作
//方式二
//可以查看当前autocommit值,默认“ON”打开状态,任意一条语句都是一个事务
show variable like '%commit%';
//关闭自动事务,需要手动commit
set autocommit = off;
//JDBC方式
public class TransactionTest1 {
public static void main(String[] args) throws Exception {
//修改id=2的money
String sql = "update account set money=100 where id=2";
Connection con = jdbcUtils.getConnection();
con.setAutoCommit(false);//开启事务
Statement st = con.createStatement();
st.executeUpdate(sql);
con.rollback();//事务回滚
con.commit();//事务提交
st.close();
con.close();
}
}
事务特性
- 原子性
- 一致性
- 隔离性
- 持久性
事务隔离性的设置语句
- 脏读 dirty read 指一个事务读取了另一个事务未提交的数据
- 不可重复读 指在一个事务内读取表中的某一行数据,多次读取结果不同(update)
- 虚读 指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致( insert )
- 丢失更新 lost update 后提交的事务把先提交的事务给覆盖掉
在MySql中设置事务隔离级别
select @@tx_isolation 查询当前事务隔离几倍
默认级别是 Repeatable read.
set session transaction isolation level 设置事务隔离级别
比如:set session transaction isolation level read uncommitted;
在jdbc中设置事务隔离级别
调用java.sql.Connection中的方法
void setTransactionIsolation(int level);
- level取值可以看API,设置的有常量
演示
//演示脏读dirty read,一个事务读到另一个事务未提交的数据
set session transaction isolation level read uncommitted; //设置隔离级别
//在A事务中
start transaction;
update account set money=money-500 where name='aaa';
update account set money=money+500 where name='bbb'; //先不提交事务
//在B事务中
start transaction;
select * from account; //此时,B事务读取到变化后的数据,就出现了脏读
//当A事务提交前,执行rollback,commit. B事务再查询就会发现,数据恢复原样。
//出现两次查询结果不一致问题,出现了不可重复读。
//解决脏读问题,将事务的隔离级别设置为read committed;
set session transaction isolation level read committed; //设置隔离级别
//此时解决了脏读,但还存在不可重复读,两次读取的数据不一样。A事务commit前和commit后
//解决不可重复读,设置隔离级别为repeatable read;
set session transaction isolation level repeatable read; //设置隔离级别
//当A事务提交后,B事务查询的与上次提交前的结果相同。
//Serializable可以解决所有问题
//该设置可以锁表,A事务未提交前,其它事务无法对此表进行操作。性能较差
代码
//jsp
//ServletAccount.java
public class ServletAccount extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
//得到请求参数
String accountIn = request.getParameter("accountin");
String accountOut = request.getParameter("accountout");
double money = Double.parseDouble(request.getParameter("money"));
//调用service
AccountService service = new AccountService();
try {
service.account(accountIn,accountOut,money);
response.getWriter().write("转账成功");
} catch (Exception e) {
e.printStackTrace();
response.getWriter().write("转账失败");
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
//AccountService.java
public class AccountService {
//汇款方法
public void account(String accountIn, String accountOut, double money) throws Exception {
//调用AccountDao中的两个方法
AccountDao dao = new AccountDao();
Connection con = null;
try {
con = jdbcUtils.getConnection();
//开启事务
con.setAutoCommit(false);
dao.accountIn(con,accountIn,money);
dao.accountOut(con,accountOut,money);
} catch (Exception e) {
e.printStackTrace();
//出现问题,进行事务回滚
if(con!=null){
try {
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
throw e;
}finally{
//事务提交
if(con!=null){
try {
con.commit();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
//AccountDao.java
public class AccountDao {
//转入
public void accountIn(Connection con, String accountIn, double money) throws SQLException, AccountException {
String sql = "update account set money=money+? where name=?";
PreparedStatement pst = con.prepareStatement(sql);
pst.setDouble(1, money);
pst.setString(2, accountIn);
int row = pst.executeUpdate();
if(row == 0){
throw new AccountException("转入失败");
}
pst.close();
}
//转出
public void accountOut(Connection con, String accountOut, double money) throws SQLException, AccountException {
String sql = "update account set money=money-? where name=?";
PreparedStatement pst = con.prepareStatement(sql);
pst.setDouble(1, money);
pst.setString(2, accountOut);
int row = pst.executeUpdate();
if(row == 0){
throw new AccountException("转出失败");
}
pst.close();
}
}
//自定义异常 AccountException.java
public class AccountException extends Exception{
public AccountException() {
super();
}
public AccountException(String message, Throwable cause) {
super(message, cause);
}
public AccountException(String message) {
super(message);
}
public AccountException(Throwable cause) {
super(cause);
}
}
用ThreadLocal解决问题
如何在另一个类中的两个方法中共享另一个类中的Connection,比如
public interface AccountDao{
//这两个方法都需要使用同一个Connection对象,需要从调用方法的地方传递,
//但没有传参,此时就要用ThreadLocal来解决
public void accountOut(String accountOut, double money) throws Exception;
public void accountIn(String accountInt, double monye) throws Exception;
}
丢失更新
解决方案详解:
- 悲观锁(假设丢失更新一定会发生)
- 利用数据库内部锁机制,管理事务
- 允许一张数据表添加多个共享锁,只能添加一个排他锁
- 事务在修改记录过程中,锁定记录,别的事务无法并发修改
- update 语句默认添加排他锁
- 乐观锁(假设丢失更新不会发生)
- 采用程序中添加版本字段解决丢失更新问题
- timestamp 时间戳自动更新
create table product(
id int,
name varchar(20),
updatetime timestamp; //时间戳
);
// timastamp 在插入和修改时,都会自动更新当前时间
//如果读取时版本字段与修改时版本字段不一致,说明别人进行修改过数据