您的当前位置:首页正文

MYSQL--悲观锁与乐观锁

2024-11-07 来源:个人技术集锦

1 悲观锁,排他锁,当数据上锁以后,其他线程不能去操作该数据,只能等待占用线程尽快结束占用。这种方法并发效率低。
测试代码:

@Autowired
    private DataSourceTransactionManager txManager;
    /**
     * 悲观锁
     */
    @Test
    public void testPessimisticLock(){
        //创建固定线程数为6的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        executorService.submit(()->{
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            // 事物隔离级别,开启新事务
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            // 获得事务状态
            TransactionStatus status = txManager.getTransaction(def);
            logger.info("锁住id为1的用户信息");
            userDao.selectUserInfoAndLock(1);
            logger.info("开始睡眠");
            try {
                TimeUnit.SECONDS.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            txManager.commit(status);
            logger.info("睡眠结束,即提交请求,锁释放");
        });
        executorService.submit(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("开始修改数据数据");
            User updateUser=new User();
            updateUser.setId(1);
            updateUser.setName("456");
            userDao.updateByPrimaryKeySelective(updateUser);
            logger.info("结束修改数据");
        });
        executorService.shutdown();
        while(true){
            if(executorService.isTerminated()){
                break;
            }
        }
        logger.info("测试完成");
    }

锁住的SQL

  <select id="selectUserInfoAndLock" parameterType="java.lang.Integer"
          resultType="com.test.model.User">
    select
    <include refid="Base_Column_List" />
    from user
    WHERE 1=1
    AND id=#{id}
    FOR UPDATE
  </select>

执行结果:

2019-06-21 18:15:42.502  INFO 18368 --- [pool-2-thread-1] com.test.service.UserServiceTest         : 锁住id为1的用户信息
2019-06-21 18:15:42.523 DEBUG 18368 --- [pool-2-thread-1] c.t.dao.UserDao.selectUserInfoAndLock    : ==>  Preparing: select id, uuid, `name`, `password` from user WHERE 1=1 AND id=? FOR UPDATE 
2019-06-21 18:15:42.637 DEBUG 18368 --- [pool-2-thread-1] c.t.dao.UserDao.selectUserInfoAndLock    : ==> Parameters: 1(Integer)
2019-06-21 18:15:42.647 DEBUG 18368 --- [pool-2-thread-1] c.t.dao.UserDao.selectUserInfoAndLock    : <==      Total: 1
2019-06-21 18:15:42.647  INFO 18368 --- [pool-2-thread-1] com.test.service.UserServiceTest         : 开始睡眠
2019-06-21 18:15:44.062  INFO 18368 --- [pool-2-thread-2] com.test.service.UserServiceTest         : 开始修改数据数据
2019-06-21 18:15:44.104 DEBUG 18368 --- [pool-2-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==>  Preparing: update `user` SET `name` = ? where id = ? 
2019-06-21 18:15:44.105 DEBUG 18368 --- [pool-2-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==> Parameters: 456(String), 1(Integer)
2019-06-21 18:16:12.648  INFO 18368 --- [pool-2-thread-1] com.test.service.UserServiceTest         : 睡眠结束,即提交请求,锁释放
2019-06-21 18:16:12.673 DEBUG 18368 --- [pool-2-thread-2] c.t.d.U.updateByPrimaryKeySelective      : <==    Updates: 1
2019-06-21 18:16:12.674  INFO 18368 --- [pool-2-thread-2] com.test.service.UserServiceTest         : 结束修改数据
2019-06-21 18:16:12.682  INFO 18368 --- [           main] com.test.service.UserServiceTest         : 测试完成

锁住的线程不添加,导致正常的修改无法执行,陷入阻塞,干扰线程执行完成,锁释放,正常的修改线程可以修改数据库信息,

扣减库存的场景

    /**
     * 悲观锁
     *
     * @param goodsId
     * @param num
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void buy2(int goodsId, int num) {
        //判断库存
        GoodsDO goodsDO = goodsMapper.selectByPrimaryKeyLock(goodsId);
        if (goodsDO.getStock() <= 0) {
            logger.info("已经售完,id{}", goodsId);
            return;
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //减库存
        int newStock = goodsDO.getStock() - num;
        int result = goodsMapper.updateStockById(goodsId, newStock);
        if (result > 0) {
            //提加订单
            OrderDO orderDO = new OrderDO();
            orderDO.setName(goodsDO.getName());
            orderDO.setNum(num);
            orderMapper.insertSelective(orderDO);

            logger.info("购买成功,id{}", goodsId);
        } else {
            logger.info("购买失败");
        }
    }

上锁sql

    <!-- 上锁-->
    <select id="selectByPrimaryKeyLock" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from t_goods
        where id = #{id,jdbcType=INTEGER}
        for update
    </select>

更新库存

    <update id="updateStockById">
        UPDATE t_goods
        SET stock=#{newStock}
        WHERE id=#{id}
    </update>

2 乐观锁,安全性比悲观锁低,并发效率高。读的时候多的情况应该使用乐观锁。

/**
     * 乐观锁
     */
    @Test
    public void testOptimisticLock(){
        executorService.submit(()->{
            int count=0;
            while(count<10){
                count++;
                logger.info("正常线程,开始第{}次尝试修改数据",count);
                User user = userDao.selectByPrimaryKey(1);
                //模拟耗时操作
                logger.info("正常线程,开始睡眠");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                logger.info("正常线程,睡眠结束");
                User updateUser=new User();
                updateUser.setId(user.getId());
                updateUser.setName("456");
                logger.info("正常线程,开始尝试修改name字段");
                if(userDao.updateUserByIdAndUuid(updateUser,user.getUuid())==1){
                    logger.info("正常线程,尝试修改name字段成功--------------");
                    break;
                }else{
                    logger.info("正常线程,尝试修改name字段失败------------------");
                }
            }
        });

        executorService.submit(()->{
            int count=0;
            while(count<3){
                count++;

                User user=new User();
                user.setId(1);
                String uuid = UUID.randomUUID().toString().replaceAll("-", "");
                user.setUuid(uuid);
                logger.info("当前为搞破坏线程,第{}次修改UUID",count);
                userDao.updateByPrimaryKeySelective(user);
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            logger.info("当前为搞破坏线程,不再修改UUID");
        });
        executorService.shutdown();
        while(true){
            if(executorService.isTerminated()){
                break;
            }
        }
        logger.info("测试完成");
    }

一个线程执行修改uuid字段,起到干扰的作用,正常的线程无法修改成功,干扰线程执行完成,正常线程执行完成。
执行结果:

2019-06-21 17:38:45.455  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,开始第1次尝试修改数据
2019-06-21 17:38:46.083 DEBUG 10456 --- [pool-1-thread-1] com.test.dao.UserDao.selectByPrimaryKey  : ==>  Preparing: select id, uuid, `name`, `password` from `user` where id = ? 
2019-06-21 17:38:46.102 DEBUG 10456 --- [pool-1-thread-1] com.test.dao.UserDao.selectByPrimaryKey  : ==> Parameters: 1(Integer)
2019-06-21 17:38:46.110 DEBUG 10456 --- [pool-1-thread-1] com.test.dao.UserDao.selectByPrimaryKey  : <==      Total: 1
2019-06-21 17:38:46.111  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,开始睡眠
2019-06-21 17:38:46.163  INFO 10456 --- [pool-1-thread-2] com.test.service.UserServiceTest         : 当前为搞破坏线程,第1次修改UUID
2019-06-21 17:38:46.196 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==>  Preparing: update `user` SET uuid = ? where id = ? 
2019-06-21 17:38:46.197 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==> Parameters: 1517bfcc69d2424683f8df72e47fa073(String), 1(Integer)
2019-06-21 17:38:46.228 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : <==    Updates: 1
2019-06-21 17:38:48.228  INFO 10456 --- [pool-1-thread-2] com.test.service.UserServiceTest         : 当前为搞破坏线程,第2次修改UUID
2019-06-21 17:38:48.228 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==>  Preparing: update `user` SET uuid = ? where id = ? 
2019-06-21 17:38:48.229 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==> Parameters: 22a5ca2858a54519ab269de3b0c55006(String), 1(Integer)
2019-06-21 17:38:48.278 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : <==    Updates: 1
2019-06-21 17:38:50.279  INFO 10456 --- [pool-1-thread-2] com.test.service.UserServiceTest         : 当前为搞破坏线程,第3次修改UUID
2019-06-21 17:38:50.280 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==>  Preparing: update `user` SET uuid = ? where id = ? 
2019-06-21 17:38:50.280 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : ==> Parameters: 305369d203324cf699f630596b494687(String), 1(Integer)
2019-06-21 17:38:50.339 DEBUG 10456 --- [pool-1-thread-2] c.t.d.U.updateByPrimaryKeySelective      : <==    Updates: 1
2019-06-21 17:38:51.112  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,睡眠结束
2019-06-21 17:38:51.112  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,开始尝试修改name字段
2019-06-21 17:38:51.118 DEBUG 10456 --- [pool-1-thread-1] c.t.dao.UserDao.updateUserByIdAndUuid    : ==>  Preparing: update `user` SET `name` = ? where 1=1 AND id = ? AND uuid=? 
2019-06-21 17:38:51.124 DEBUG 10456 --- [pool-1-thread-1] c.t.dao.UserDao.updateUserByIdAndUuid    : ==> Parameters: 456(String), 1(Integer), 5ca0168bfd8d470190c262584732d8d0(String)
2019-06-21 17:38:51.125 DEBUG 10456 --- [pool-1-thread-1] c.t.dao.UserDao.updateUserByIdAndUuid    : <==    Updates: 0
2019-06-21 17:38:51.125  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,尝试修改name字段失败------------------
2019-06-21 17:38:51.125  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,开始第2次尝试修改数据
2019-06-21 17:38:51.125 DEBUG 10456 --- [pool-1-thread-1] com.test.dao.UserDao.selectByPrimaryKey  : ==>  Preparing: select id, uuid, `name`, `password` from `user` where id = ? 
2019-06-21 17:38:51.125 DEBUG 10456 --- [pool-1-thread-1] com.test.dao.UserDao.selectByPrimaryKey  : ==> Parameters: 1(Integer)
2019-06-21 17:38:51.126 DEBUG 10456 --- [pool-1-thread-1] com.test.dao.UserDao.selectByPrimaryKey  : <==      Total: 1
2019-06-21 17:38:51.127  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,开始睡眠
2019-06-21 17:38:52.339  INFO 10456 --- [pool-1-thread-2] com.test.service.UserServiceTest         : 当前为搞破坏线程,不再修改UUID
2019-06-21 17:38:56.128  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,睡眠结束
2019-06-21 17:38:56.128  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,开始尝试修改name字段
2019-06-21 17:38:56.129 DEBUG 10456 --- [pool-1-thread-1] c.t.dao.UserDao.updateUserByIdAndUuid    : ==>  Preparing: update `user` SET `name` = ? where 1=1 AND id = ? AND uuid=? 
2019-06-21 17:38:56.129 DEBUG 10456 --- [pool-1-thread-1] c.t.dao.UserDao.updateUserByIdAndUuid    : ==> Parameters: 456(String), 1(Integer), 305369d203324cf699f630596b494687(String)
2019-06-21 17:38:56.223 DEBUG 10456 --- [pool-1-thread-1] c.t.dao.UserDao.updateUserByIdAndUuid    : <==    Updates: 1
2019-06-21 17:38:56.223  INFO 10456 --- [pool-1-thread-1] com.test.service.UserServiceTest         : 正常线程,尝试修改name字段成功--------------
2019-06-21 17:38:56.224  INFO 10456 --- [           main] com.test.service.UserServiceTest         : 测试完成

扣减库存场景-外层加重试机制

    /**
     * 乐观锁
     *
     * @param goodsId
     * @param num
     * @throws Exception
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void buy3(int goodsId, int num) throws Exception {
        logger.info("----------------------开始测试---------------------------");
        //判断库存
        GoodsDO goodsDO = goodsMapper.selectByPrimaryKey(goodsId);
        if (goodsDO.getStock() <= 0) {
            logger.info("已经售完,id{}", goodsId);
            return;
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //减库存
        int newStock = goodsDO.getStock() - num;
        int result = goodsMapper.updateStockByIdAndVersion(goodsId, newStock, goodsDO.getVersion(), UUIDUtils.getUUID());
        if (result > 0) {
            //提加订单
            OrderDO orderDO = new OrderDO();
            orderDO.setName(goodsDO.getName());
            orderDO.setNum(num);
            orderMapper.insertSelective(orderDO);

            logger.info("购买成功,id{}", goodsId);
        } else {
            logger.info("购买失败");
            throw new Exception("购买失败");
        }
    }

加版本号更新

    <update id="updateStockByIdAndVersion">
        UPDATE t_goods
        SET stock=#{newStock},
        version=#{newVersion}
        WHERE id=#{id}
        AND version=#{oldVersion}
    </update>

扣减库存当前读更新

    /**
     * mysql当前读
     *
     * @param goodsId
     * @param num
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void buy1(int goodsId, int num) {
        //判断库存
        GoodsDO goodsDO = goodsMapper.selectByPrimaryKey(goodsId);
        if (goodsDO.getStock() <= 0) {
            logger.info("已经售完,id{}", goodsId);
            return;
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //减库存
        int result = goodsMapper.reduceStockById(goodsId, num);
        if (result > 0) {
            //提加订单
            OrderDO orderDO = new OrderDO();
            orderDO.setName(goodsDO.getName());
            orderDO.setNum(num);
            orderMapper.insertSelective(orderDO);

            logger.info("购买成功,id{}", goodsId);
        } else {
            logger.info("购买失败");
        }
    }

更新,sql中对库存判断

    <update id="reduceStockById">
        UPDATE t_goods
        SET stock=stock-#{num}
        WHERE id=#{id}
        AND stock>=#{num}
    </update>

参考:https://mp.weixin.qq.com/s/ZJdfthAg2tImArzJO7bPaQ

Top