zoukankan      html  css  js  c++  java
  • 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 ,如果查询条件带有主键,会锁行数据,如果没有,会锁表,另外并发访问下可能会造成死锁!

    一颗安安静静的小韭菜。文中如果有什么错误,欢迎指出。
  • 相关阅读:
    每日一练leetcode
    java 中 int与string的相互转化
    每日一练leetcode
    每日一题leetcode
    每日一练leetcode
    每日一练leetcode
    每日一题leetcode
    Three20在IOS6中不能正常使用 迎客
    苹果提供的支付功能接口 迎客
    ios随记 迎客
  • 原文地址:https://www.cnblogs.com/c-Ajing/p/13448338.html
Copyright © 2011-2022 走看看