Optimistic locking based on database

An introduction to optimistic locking and pessimistic locking

insert image description here

Pessimistic lock:

Pessimistic locks can implement serialized execution of data. For example, syn and lock are representatives of pessimistic locks. At the same time, pessimistic locks can be subdivided into fair locks, unfair locks, reentrant locks, etc.

Optimistic lock:

Optimistic locking is generally implemented using the version number mechanism or the CAS algorithm. There are relatively more CAS algorithms, so special attention is required here.

  • version number mechanism

Generally, a data version number version field is added to the data table to indicate the number of times the data has been modified. When the data is modified, the version value will increase by one. When thread A wants to update the data value, it will also read the version value while reading the data. When submitting the update, if the version value just read is equal to the version value in the current database, it will be updated, otherwise retry Update operations until the update succeeds.

  • CAS algorithm

A typical representative of optimistic locking: cas, using cas for lock-free mechanism locking, var5 is the memory value read before the operation, var1+var2 in while is the estimated value, if the estimated value == memory value, then It means that it has not been modified in the middle, and at this time, the new value is replaced by the memory value. The do while is to perform the spin operation again when the operation fails, that is, to operate the previous logic again.

int var5;
do {
    
    
   var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

Optimistic lock optimistic lock and pessimistic lock based on database method and Redis method

Two optimistic locking practice cases

2.1 The problem of oversold inventory recurred

2.1.1 Simulation analysis of flash sales orders

Things to think about when placing an order in seckill:

When placing an order, you need to judge two points:

  • Whether the flash sale has started or ended, if it has not started or has ended, you cannot place an order
  • Whether the inventory is sufficient, if not enough, the order cannot be placed

Order core logic analysis:

When the user starts to place an order, we should query the coupon information, find out the coupon information, and judge whether the flash sale conditions are met

For example, whether the time is sufficient, if the time is sufficient, further judge whether the inventory is sufficient, if both are satisfied, then deduct the inventory, create an order, and then return the order id, if one of the conditions is not met, it will end directly.
insert image description here

2.1.2 Lightning code

@Override
    public Result seckillVoucher(Long voucherId) {
    
    
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            // 库存不足
            return Result.fail("库存不足!");
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
    
    
            //扣减库存
            return Result.fail("库存不足!");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id
        Random ra =new Random();
        long orderId = ra.nextLong();
        voucherOrder.setId(orderId);
        // 6.2.用户id
        Long userId =  ra.nextLong();
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        return Result.ok(orderId);

    }

2.1.3 Unit test results

@SpringBootTest
class LockApplicationTests {
    
    

    //实际项目中应使用自定义的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(100);
    @Autowired
    private IVoucherOrderService voucherOrderService;


    @Test
    void testIdWorker() throws InterruptedException {
    
    

        CountDownLatch latch = new CountDownLatch(100);

        Runnable task = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Result result = voucherOrderService.seckillVoucher(2L);
                System.out.println("result = " + result);
            latch.countDown();
            }
        } ;
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
    
    
            threadPool.execute(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end - begin));
    }
}

Observe the ideal console
insert image description here

Observe the data in the table

insert image description here

2.2 Analysis of oversold inventory

Analysis of the overselling problem: this is written in our original code

 if (voucher.getStock() < 1) {
    
    
        // 库存不足
        return Result.fail("库存不足!");
    }
    //5,扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update();
    if (!success) {
    
    
        //扣减库存
        return Result.fail("库存不足!");
    }

Suppose thread 1 comes to check the inventory, judges that the inventory is greater than 1, and is about to deduct the inventory, but has not had time to deduct it. At this time, thread 2 comes, and thread 2 also checks the inventory, and finds that the quantity must be greater than 1, then These two threads will deduct the inventory, and finally multiple threads are equivalent to deducting the inventory together. At this time, the problem of oversold inventory will appear.
insert image description here

2.3 Optimistic lock solves oversold problem

Use pessimistic locks to solve thread safety problems under high concurrency conditions. Some are too heavy. Click to lock the entire business. Only the thread that grabs the lock can be exclusive, and other threads can only wait. This is obviously a bit overbearing; use optimistic locks The way to judge whether there are other threads to modify the data when updating the data, does not affect the efficiency in the case of high concurrency;

2.3.1 Version number method

The operation logic is to perform +1 operation on the version number during the operation, and then require the version to be 1 to operate, then after the first thread operates, the version in the database becomes 2, but he himself satisfies version=1, so there is no problem. At this time, thread 2 executes, and thread 2 needs to add the condition version=1 at the end, but now thread 1 has already operated, so thread 2 does not satisfy the condition of version=1 when operating Therefore, thread 2 cannot be successfully executed.
insert image description here
Since the scenario in our case is to reduce inventory, the solution is a bit special, so the version number can be replaced by the stock field stock;When updating, add a conditional judgment: compare the queried version number with the version number in the table, if they are equal, the update is successful, and if they are not equal, the update fails

Modify the code scheme 1.

When VoucherOrderServiceImpl deducts inventory, change it to:

boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1") //set stock = stock -1
            .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

The core meaning of the above logic is: as long as the inventory when I deduct the inventory is the same as the inventory I queried before, it means that no one has modified the inventory in the middle, then it is safe at this time, but the above method passes The test found that there will be many failures. The reason for the failure is: in the process of using optimistic locking, it is assumed that 100 threads have obtained 100 inventory at the same time, and then everyone will deduct it together, but only 1 of the 100 people can deduct it. Success, when other people are processing, when they are deducting, the inventory has been modified, so other threads will fail at this time;

insert image description here
In other scenarios, it is no problem to use the version number scheme with a low success rate; but this is a spike scenario, and the low success rate does not meet business requirements;

Modify the code scheme 2.

The previous method should be consistent before and after modification, but we have analyzed that the probability of success is too low, so our optimistic lock needs to be changed to a stock greater than 0.

boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).gt("stock", 0).update(); //where id = ? and stock > 0

insert image description here
After the inventory is sold out, other threads will return the prompt of insufficient inventory if they spike again!
insert image description here
Small expansion of knowledge:

For the excessive spin pressure in cas, we can use the Longaddr class to solve it

An improved class of AtomicLong provided by Java8, LongAdder

When a large number of threads concurrently update an atomicity, the natural problem is spin, which will cause concurrency problems. Of course, this is better than using syn directly.

So use such a class, LongAdder to optimize

If a value is obtained, the value of cell and base will be incremented, and finally a complete value
knowledge extension will be returned:

For the excessive spin pressure in cas, we can use the LongAdder class to solve an improved AtomicLong class provided by Java8. When a large number of LongAdder threads update an atomicity concurrently, the natural problem is spin, which will cause Concurrency issues, of course, this is better than using syn directly,
so use such a class, LongAdder to optimize
If you get a certain value, the value of cell and base will be incremented, and finally return a complete value
insert image description here
LongAdder principle Analysis

Case source code

Case source code

sql script in the case

tb_voucher_order: order table
tb_seckill_voucher: coupon inventory, start time of rush purchase, end time of rush purchase.

  • tb_seckill_voucher
DROP TABLE IF EXISTS `tb_seckill_voucher`;
CREATE TABLE `tb_seckill_voucher`  (
  `voucher_id` bigint(20) UNSIGNED NOT NULL COMMENT '关联的优惠券的id',
  `stock` int(8) NOT NULL COMMENT '库存',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `begin_time` datetime NOT NULL COMMENT '生效时间',
  `end_time` datetime NOT NULL COMMENT '失效时间',
  PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀优惠券表,与优惠券是一对一关系' ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of tb_seckill_voucher
-- ----------------------------
INSERT INTO `tb_seckill_voucher` VALUES (2, 100, '2022-01-04 10:09:17', '2023-05-23 03:07:53', '2023-05-23 09:09:04', '2023-05-31 10:09:17');

SET FOREIGN_KEY_CHECKS = 1;

  • tb_voucher_order
DROP TABLE IF EXISTS `tb_voucher_order`;
CREATE TABLE `tb_voucher_order`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `user_id` bigint(20) UNSIGNED NOT NULL COMMENT '下单的用户id',
  `voucher_id` bigint(20) UNSIGNED NOT NULL COMMENT '购买的代金券id',
  `pay_type` tinyint(1) UNSIGNED ZEROFILL NULL DEFAULT NULL COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
  `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
  `pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
  `use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
  `refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of tb_voucher_order
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

Guess you like

Origin blog.csdn.net/weixin_43811057/article/details/130816043