mysql并发更新丢失问题解决方案

mysql并发更新丢失问题解决方案

  • 问题展示
    • ssm项目主要代码
    • idea多线程debug模拟并发更新
  • 解决方案
    • 悲观锁
      • 意向共享锁
      • 意向排它锁
    • 乐观锁

问题展示

搭建了一个简单springboot的ssm项目,通过idea提供的多线程debug模式模拟并发更新丢失数据问题。业务是根据name查询出来total,然后再根据name更新total+1

ssm项目主要代码

controller代码

@RestController
public class UserController {
    
    @Autowired
    private UserService service;
    
    @GetMapping("user/{id}")
    public String getUser(@PathVariable("id") int id){
        Map<String, Object> user = service.getUserById(id);
        System.out.println(user);
        return user.get("name").toString();
    }

    @GetMapping("test/concurrentUpdateLose")
    public String concurrentUpdateLose() {

        service.concurrentUpdateLose("zhangsan");
        return "ok";
    }
}

service代码

public interface UserService {
    Map getUserById(int id);

    int concurrentUpdateLose(String name );
}

service实现类代码

@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserDao dao;


    public Map getUserById(int id){
        return dao.getUserById("lisi");
    }


    /**
     * 原始查询并更新
     */
    @Transactional
    @Override
    public int concurrentUpdateLose(String name ) {

        Map userById = dao.getUserById(name);
        int updateResult = getUpdateResult(userById);
        return updateResult;
    }

    private int getUpdateResult(Map userById) {

        int oldTotal = (Integer)(userById.get("total"));
        Date time = (Date)(userById.get("time"));
        int newTotal = oldTotal + 1;
        String name = (String) userById.get("name");
        /*try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

        return dao.updateUserTotalByid(name, newTotal,time);
    }
}

dao层代码

@Mapper
public interface UserDao {
       @Select("select id, name,age,total,time from user where name = #{name}")
    Map getUserById(String name);

    @Update("update user set total = #{newTotal} where name = #{name}")
    int updateUserTotalByid(String name,int newTotal, Date time);
}

springboot启动类代码

@SpringBootApplication
@MapperScan(basePackages="xxx.xxx")
public class SsmDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SsmDemoApplication.class, args);
    }

}

数据库测试表

mysql并发更新丢失问题解决方案_第1张图片

idea多线程debug模拟并发更新

设置方式如图,将all改为thread
mysql并发更新丢失问题解决方案_第2张图片

启动项目,浏览器或者postman工具调用接口
mysql并发更新丢失问题解决方案_第3张图片

debug走一行查询语句,先不走更新语句,此时第二次调用接口,同样走一行查询语句,不走更新语句。为的是让两次调用的查询结果一致。
第一次调用
mysql并发更新丢失问题解决方案_第4张图片
第二次调用
mysql并发更新丢失问题解决方案_第5张图片
可以看到两次调用的线程不一样,查询结果都是total=1
之后分别走完update语句,因为都是更新的total=2,所以虽然调用了两次total+1,但是结果是更新完结果是total=2,而不是预期结果total=3.

数据库结果是2
mysql并发更新丢失问题解决方案_第6张图片

解决方案

悲观锁

意向共享锁

select 语句加上lock in share mode
结果:会造成死锁(等待锁时间超时,会自动释放锁)。

​ 原因:意向共享锁可以允许别的事务读,可继续加共享意向锁,但不允许别的事务修改。

​ 不采用此方案

意向排它锁

select语句加上for update
结果:可以解决并发更新丢失问题。

​ 原因:意向排它锁,不允许别的事务加S锁或X锁,直到加锁的事务执行完事务释放锁,相当于每个事务串行执行,自然不存在并发更新丢失问题。

​ 不采用,效率太低。

乐观锁

对要并发更新的表,添加版本号(可以采用mysql自动更新的时间戳,timestamp)。查询时查到该行数据的版本号(时间戳),更新时where条件带上“ and timestamp=xxx”,同时获取到更新结果成功的条数,(mybatis或jdbc带有此功能)。如果一个事务更新成功,则该update语句执行成功的结果是1,同时其他事务更新就会失败,执行成功条数的结果是0,因为一旦有事务更新该条记录成功,那么该条记录的版本号(时间戳)就会改变,因为更新语句中带有条件“ and timestamp=xxx”,所以更新会失败。

为了保证所有的事务最终都能打到更新的目的,需要对更新失败的事务做处理。如果更新结果为0,则循环执行事务,即重新查询该条记录,用最新的版本号作为更新条件。

这里涉及到mysql的隔离级别,在读提交(RC)级别下,可以读到别的事务已提交的结果,所以可以在一个事务中循环执行“先查询,再根据查询结果更新”。
RC级别,通过@Transactional(isolation = Isolation.READ_COMMITTED)设置会话事务的隔离级别。

而在可重复读(RR)级别下,不能读到别的事务已提交的结果,所以需要循环开启新的事务来执行流程。这里暂未测试

下面已RC级别演示解决方案。修改后的代码如下

service代码

@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserDao dao;


    public Map getUserById(int id){
        return dao.getUserById("lisi");
    }

    /**
     * 乐观锁解决方案,数据库隔离级别设置为读提交,这样在一个事务里可以读到其他事务已提交的变更
     */
    @Transactional(isolation = Isolation.READ_COMMITTED)
    @Override
    public int concurrentUpdateLose(String name ) {
        int updateResult=0;
        int cyclicCount = 0;
        do {
            Map userById = dao.getUserById(name);
            updateResult = getUpdateResult(userById);
            cyclicCount++;
        }while (updateResult == 0 && cyclicCount<10);
        return updateResult;
    }

    

    private int getUpdateResult(Map userById) {

        int oldTotal = (Integer)(userById.get("total"));
        Date time = (Date)(userById.get("time"));
        int newTotal = oldTotal + 1;
        String name = (String) userById.get("name");
        /*try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

        return dao.updateUserTotalByid(name, newTotal,time);
    }
}

dao层代码

@Mapper
public interface UserDao {
    
//    @Select("select id, name,age,total,time from user where name = #{name} for update")
    @Select("select id, name,age,total,time from user where name = #{name}")
    Map getUserById(String name);


    @Update("update user set total = #{newTotal} where name = #{name} and time = #{time}")
    int updateUserTotalByid(String name,int newTotal, Date time);
}

现在将代码改为更新时添加了一个查询条件time,time值是查询结果的time值。time是数据库表自动更新的时间戳,所以每次更新都会更新time,相当于版本号。

修改后,更新结果为total=3,演示流程步骤同上。

建议采用乐观锁的方案

你可能感兴趣的:(mysql,开发,mysql,java)