MySQL数据库update A set num=num+ ? where id=?并发执行情况下存在线程安全问题吗?

首先,先说结论:这个SQL语句执行可以看做是原子操作,没有线程安全问题

使用代码演示一下:

1.创建一个xu_hao表,为了简单,只有id和num两个字段

CREATE TABLE `xu_hao`  (
  `id` int(11) NOT NULL,
  `num` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.插入一条初始化数据:

INSERT INTO `xu_hao`(`id`, `num`) VALUES (1, 0);

3.创建一个spring boot工程,项目结构如下:
在这里插入图片描述
4.导入maven依赖:

	<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

5.编写application.yml配置文件,测试项目,简单配置即可:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/learn_work?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    
logging:
  level:
      root: warn
      com.learn.spmysqltest.mapper: trace

6.创建一个XuHaoMapper:

public interface XuHaoMapper {
    /**
     * 自增+1的方式更新num
     *
     * @param id
     * @return
     */
    @Update("update xu_hao set num = num + 1 where id = #{id}")
    int updateXuHaoByIncrease(int id) throws Exception;

     /**
     * 通过id直接更新num
     *
     * @param num
     * @param id
     * @return
     * @throws Exception
     */
    @Update("update xu_hao set num = #{num} where id = #{id}")
    int updateXuHao(int num, int id) throws Exception;

    /**
     * 通过id查询num
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Select("select num from xu_hao where id = #{id} ")
    int selectXuHao(int id) throws Exception;
}

7.创建XuHaoService:

public interface XuHaoService {

    /**
     * 自增+1的方式更新num
     *
     * @param id
     * @return
     */
    int updateXuHaoByIncrease(int id) throws Exception;

    /**
     * 直接更新num
     *
     * @param id
     * @throws Exception
     */
    int updateXuHao(int id) throws Exception;

    /**
     * 通过id查询num
     *
     * @param id
     * @return
     * @throws Exception
     */
    int getXuHao(int id) throws Exception;

}

8.创建XuHaoServiceImpl:

@Service
public class XuHaoServiceImpl implements XuHaoService {

    @Autowired
    private XuHaoMapper xuHaoMapper;

    @Override
    @Transactional
    public int updateXuHaoByIncrease(int id) throws Exception {
        return xuHaoMapper.updateXuHaoByIncrease(id);
    }

    @Override
    @Transactional
    public int updateXuHao(int id) throws Exception {
        int dbNum = xuHaoMapper.selectXuHao(id);
        dbNum++; // num + 1
        return xuHaoMapper.updateXuHao(dbNum, id);
    }

    @Override
    @Transactional(readOnly = true)
    public int getXuHao(int id) throws Exception {
        return xuHaoMapper.selectXuHao(id);
    }
}

9.开启事务支持:

@SpringBootApplication
@EnableTransactionManagement // 开启事务
@MapperScan("com.learn.spmysqltest.mapper")
public class SpMysqlTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpMysqlTestApplication.class, args);
    }
}

由于单元测试多线程可能会有问题,为了简单,编写一个XuHaoController进行测试:

@RestController
@RequestMapping("/xuhao")
public class XuHaoController {

    @Autowired
    private XuHaoService xuHaoService;

    /**
     * 测试 update xu_hao set num = num + 1 where id = 1
     *
     * @return
     */
    @GetMapping("/updateadd1")
    public String updateByIncrease() {
        int cupNum = Runtime.getRuntime().availableProcessors(); // 获取可用的处理器个数
        CountDownLatch latch = new CountDownLatch(cupNum);
        for (int i = 0; i < cupNum; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                    xuHaoService.updateXuHaoByIncrease(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }
        return "cupNum=" + cupNum + ",预期num=" + cupNum;
    }

    /**
     * num = num + 1
     * 测试 update xu_hao set num = num  where id = 1
     *
     * @return
     */
    @GetMapping("/update")
    public String update() {
        int cupNum = Runtime.getRuntime().availableProcessors(); // 获取可用的处理器个数
        CountDownLatch latch = new CountDownLatch(cupNum);
        for (int i = 0; i < cupNum; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                    xuHaoService.updateXuHao(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }
        return "cupNum=" + cupNum + ",预期num=" + cupNum;
    }
}

进入正式测试,访问:http://127.0.0.1:8080/xuhao/updateadd1,运行结果:
在这里插入图片描述
数据库:
在这里插入图片描述
可以看到,我电脑处理器个数是6个,我用CountDownLatch模拟6个线程同时执行数据库更新操作,预期值和处理器个数一致,并没有出现线程安全问题。

简单解释一下:每次执行update的时候,会持有id=1这行记录的排它锁,后面的update需要持有这个记录的排它锁释放之后,才能对他进行修改,也就是说,后面的update操作会阻塞,直到前一个update提交成功或者发生异常回滚事务了,他才会获得这个锁,从而对数据进行修改。(可以网上查询更加详细的解答)

如果我们是查询num后,在代码里面进行加1操作,再更新回数据库呢?把数据库中的num重新置为0,访问:http://127.0.0.1:8080/xuhao/update,数据库结果:
在这里插入图片描述
预期是6,现在却是1,很明显有线程安全问题,有其他的解决方法吗?

其实有了上面的启示,我们很容易想到,使用MySQL数据库的排它锁来解决(select … for update),在XuHaoMapper中增加一个函数:

/**
     * 通过id查询num
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Select("select num from xu_hao where id = #{id} for update ")
    int selectXuHao2(int id) throws Exception;

修改XuHaoServiceImpl中的updateXuHao方法:

	@Override
    @Transactional
    public int updateXuHao(int id) throws Exception {
        // int dbNum = xuHaoMapper.selectXuHao(id);
        int dbNum = xuHaoMapper.selectXuHao2(id);
        dbNum++; // num + 1
        return xuHaoMapper.updateXuHao(dbNum, id);
    }

重新将数据库中num置为0,并访问http://127.0.0.1:8080/xuhao/update,数据库结果:
在这里插入图片描述
可以看到,数据库结果与预期结果一致,此时也没有并发访问问题,原因与上面相似,for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则必须获取该行级锁.行锁永远是独占方式锁。for update 需要事务提交或回滚了才会释放锁,所以在spring管理的事务方法中,使用这种方式,也不会出现并发访问问题。

注意:select … for update ,如果查询条件带有主键,会锁行数据,如果没有,会锁表,另外并发访问下可能会造成死锁!

posted @ 2020-06-05 09:55  三分魔系  阅读(513)  评论(0编辑  收藏  举报