并发事务之丢失更新
7 并发事务问题之丢失更新
丢失更新:一个事务的更新被另一个事务的更新覆盖了;
时间点 | 事务1 | 事务2 |
t1 | 开始事务 | |
t2 | 开始事务 | |
t3 | 查询pid=p1的记录结果为[pid=p1,pname=zhangSan,age=23,sex=male] | |
t4 | 查询pid=p1的记录结果为[pid=p1,pname=zhangSan,age=23,sex=male] | |
t5 | 修改age=24,其它保留原值,即: update person set pname='zhangSan', age=24,sex='male' where pid='p1'; | |
t6 | 提交事务 | |
t7 | 修改sex=female,其它保留原值 update person set pname='zhangSan', age=23,sex='female' where pid='p1'; | |
t8 | 提交事务 |
事务2覆盖了事务1的更新操作。结果为:[pid=p1,pname=zhangSan,age=23,sex=female]。因为事务2没有在事务1的基础上进行更新,而是在自己的查询基础上进行更新。
public class Demo1 { private static Connection getConnection() throws Exception { String driverClassName = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/day12?useUnicode=true&characterEncoding=utf8"; String username = "root"; String password = "123";
Class.forName(driverClassName); return DriverManager.getConnection(url, username, password); }
public Person load(Connection con, String pid) throws Exception { String sql = "select * from t_person where pid=?"; PreparedStatement pstmt = con.prepareStatement(sql); pstmt.setString(1, pid); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { return new Person(rs.getString(1), rs.getString(2), rs.getInt(3), rs.getString(4)); } return null; }
public void update(Connection con, Person p) throws Exception { String sql = "update t_person set pname=?, age=?, gender=? where pid=?"; PreparedStatement pstmt = con.prepareStatement(sql); pstmt.setString(1, p.getPname()); pstmt.setInt(2, p.getAge()); pstmt.setString(3, p.getGender()); pstmt.setString(4, p.getPid());
pstmt.executeUpdate(); }
@Test public void fun1() throws Exception { Connection con = getConnection(); con.setAutoCommit(false);
//[pid=p1,pname=zs,age=24,gender=male] Person p = load(con, "p1"); p.setAge(42);//断点 update(con, p);
con.commit(); } @Test public void fun2() throws Exception { Connection con = getConnection(); con.setAutoCommit(false); //[pid=p1,pname=zs,age=24,gender=male] Person p = load(con, "p1"); p.setGender("female");//断点 update(con, p);
con.commit(); } } |
处理丢失更新:
- 悲观锁:在查询时给事务上排他锁,这可以让另一个事务在查询时等待前一个事务解锁后才能执行;
- 乐观锁:给表添加一个字段,表示版本,例如添加version字段,比较查询到的version与当前vesion是否相同;
7.1 悲观锁解决丢失更新
只需要修改上面代码的load()方法中select语句即可:
select * from t_person where pid=? for update
public Person load(Connection con, String pid) throws Exception { String sql = "select * from t_person where pid=? for update"; PreparedStatement pstmt = con.prepareStatement(sql); pstmt.setString(1, pid); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { return new Person(rs.getString(1), rs.getString(2), rs.getInt(3), rs.getString(4)); } return null; } |
悲观锁:悲观的思想,认为丢失更新问题总会出现,在select语句中添加for update为事务添加排他锁,这会让其他事务等待当前事务结束后才能访问。当然,其他事物的select语句中也要加上for update语句才会等待;
悲观锁的性能低!
7.2 乐观锁
乐观锁与数据库锁机制无关;
我们需要修改t_person表,为其添加一个字段表示当前记录的版本。例如给t_person表添加version字段,默认值为1。
当事务查询记录时得到version=1,再执行update时需要比较当前version的值是否与查询到的version相同,决定update是否执行成功。如果update成功,还要把version的值加1。
public void update(Connection con, Person p) throws Exception { String sql = "update t_person set pname=?, age=?, gender=?, version=version+1 where pid=? and version=?"; PreparedStatement pstmt = con.prepareStatement(sql); pstmt.setString(1, p.getPname()); pstmt.setInt(2, p.getAge()); pstmt.setString(3, p.getGender()); pstmt.setString(4, p.getPid()); pstmt.setInt(5, p.getVersion());
pstmt.executeUpdate(); } |
- 事务1:查询时得到version=1;
- 事务2:查询时得到version=1;
- 事务1:执行update时因为version没有改变,所以update执行成功,update不只修改了age=42,还修改了version=2;
- 事务2:执行update语句时version已经为2,而查询时的version为1,所以update执行失败;
乐观锁:与数据库锁机制无关,乐观的思想,认为丢失更新不是总出现;通过给表添加版本字段来决定update操作是否成功。即查询时和更新时的版本必须一致!