并发事务问题之丢失更新

并发事务问题之丢失更新
  丢失更新:一个事务的更新被另一个事务的更新覆盖了;

时间点 事务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是否相同;

悲观锁解决丢失更新
只需要修改上面代码的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操作是否成功。即查询时和更新时的版本必须一致!

上一篇:java根据url下载图片


下一篇:ThreadLocal的用处