数据库资源竞争原理及解决办法 (附代码和测试用例)

    资源竞争简介    

        在多线程环境中,大家耳熟能详的是死锁,活锁。还有一个容易被忽略的是资源竞争(race condition)。这三个在很大程度上是非常相似的,这里不展开详述。

        在开发过程中,相信有些人遇到过类似的场景:两个用户下单成功了,库存只少了一个。原理其实很简单,由多个线程对同一个资源进行读写引起。比如线程A读到了未提交的数据,然后继续操作这个脏数据,最后将结果写入到了数据库。

    事务和锁    

        我们很容易想到通过事务隔离机制来对避免脏读。例如通过Spring 注解。当然这是第一步,申明成事务后,它确实已经按事务运行,但部署之后仍会发现上述问题。

        这里需要注意的是,多线程环境下它可能会读到“旧”值(注意和脏读区分),例如两个线程同时进入了这个方法;或者上一个线程未完成提交,新的进程又进入了。毕竟,事务并不提供互斥锁。到这一步,接下来要做的就简单明了了

        通过锁来控制。首先排除Java内置的锁,分布式环境很难控制,粒度不好掌握也影响性能。数据库提供了不同级别的锁,库锁,表锁,行锁。在这里,我们要用的是行锁。很多做web开发的可能对行锁并不太了解,具体可以查阅官方文档。

        不同数据库的语法有些不同,MySQL中是“select *** for update”。要注意的是行锁要指定主键,否则会退化成表锁。

        这样一步步分析下来,第一种解决方法就出来了。事务+行锁。            

 public void rowLock() throws SQLException {
        Connection connection  = getDataSource().getConnection();

        boolean autoCommit = connection.getAutoCommit();
        connection.setAutoCommit(false);
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select * from stock where id = 1 and visible = 1 for update");
        resultSet.next();
        int current_value = resultSet.getInt("current_value");
        statement.execute("update stock set visible = 0 where id = 1");
        /**
         * do other things
         */
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        statement.execute("update stock set current_value ="+ (current_value-1) +" where id = 1");
        statement.execute("update stock set visible = 1 where id = 1");
        connection.commit();
        connection.setAutoCommit(autoCommit);
    }

        通过设置autocommit为false开启事务,使用“select for update”锁死一行记录,操作完成后,最后提交。

版本控制

        除了事务和锁,还有另一种解决方案,加入版本号。

        简单来说,我们的目标是避免读到过期数据,在这里,采用的办法是对操作采用一个版本号,每次操作版本号递增,如果在更新数据库的时候该数据的版本号未变化,则说明这一过程没有另外的线程在干扰。如果版本号已经变化,需要重试。

        

public int updateWithVersion(Connection connection) throws SQLException,IllegalStateException {
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select * from stock where id = 1");
        resultSet.next();
        int version = resultSet.getInt("version");
        int current_value = resultSet.getInt("current_value");

        /**
         * do other things
         */
        try {
            Thread.sleep(new Random().nextInt(100)+100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String sql = "update stock set current_value = %d,version = %d where id = 1 and version = %d";
        sql = String.format(sql,current_value-1,version+1,version);
        int result = statement.executeUpdate(sql);
        return result;

    }

完整代码附测试用例https://git.oschina.net/moyiguke/codefamily/blob/master/MysqlRaceCondition.java

猜你喜欢

转载自my.oschina.net/u/992559/blog/782034