Dark Horse Review: Coupon Spike

globally unique ID

Each store can issue coupons:

image-20230131162320940

When the user rushes to buy, the order will be generated and saved in the tb_voucher_order table, and if the order table uses the database auto-increment ID, there are some problems:

  • The regularity of id is too obvious
  • Limited by the amount of data in a single table

Scenario analysis 1 : If our id has too obvious rules, users or business opponents can easily guess some of our sensitive information, such as how many orders the mall sold in a day, which is obviously inappropriate.

Scenario analysis 2 : As the scale of our mall becomes larger and larger, the capacity of a single mysql table should not exceed 500W. After the amount of data is too large, we need to split the database and table, but after splitting the table, they logically They are the same table, so their ids cannot be the same, so we need to ensure the uniqueness of ids.

Global ID Generator

The global ID generator is a tool used to generate a globally unique ID in a distributed system, and generally must meet the following characteristics:

image-20230131162442317

In order to increase the security of the ID, instead of directly using the auto-incremented value of Redis, we can concatenate some other information:

image-20230131162600552

Components of ID : sign bit: 1bit, always 0

Timestamp : 31bit, in seconds, can be used for 69 years

Serial number : 32bit, counter within seconds, supporting 2^32 different IDs per second

Redis implements globally unique Id

Code

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {
    
    

    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
    
    
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
    
    
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2自增长
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        // 3.拼接并返回
        return timeStamp << COUNT_BITS | count;
    }
}

code analysis

Globally unique ID components: timestamp + serial number

ID 64 bits, that is Long type

Generation strategy: based on Redis self-growth

  1. generate timestamp

    • Starting time

          public static void main(String[] args) {
              
              
              LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
              long second = time.toEpochSecond(ZoneOffset.UTC);
              System.out.println("second: "+second);
          }
      
    • current time

      LocalDateTime now = LocalDateTime.now();
              long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
      

    Subtract the two to generate a timestamp

  2. generate serial number

    • Wrong way:

      Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" );
      
      • The key of each piece of data in a business is the same, such as order business, keyPrefix = order, the key of each order is the same
      • The serial number is only 32 bits, it may not be enough to write like this
    • Correct spelling:

      After key + date

      • date in year-month-day format

        • It is convenient to check the order volume of a certain year, a certain month and a certain day

        • Don't worry about the id is not enough (32 bits are enough)

              String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
              Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
      
    • Wonderful

  3. Stitching:

    Wrong way:

    • Concatenate as a string
      • Return type: Long

    Requirement: id high bit: timestamp, low bit: serial number

    Correct spelling:

    • bit operation

      • The timestamp is shifted to the left by 32 bits, and the original 32 bits are filled with 0
      • How to move the serial number to the low position of the timestamp?
      • OR operation
        • The lower (32) bits of the timestamp are all 0, and 0 and any number or operation are opposite
        • 0 or 0 = 0, 0 or 1 = 1
      • The timestamp is first shifted to the left to make room, and then the OR operation is performed to keep the serial number
            return timeStamp << 32 | count;
      

test

package com.hmdp;

import com.hmdp.entity.Shop;
import com.hmdp.service.impl.ShopServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisIdWorker;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;

@SpringBootTest
class HmDianPingApplicationTests {
    
    

    @Resource
    private RedisIdWorker redisIdWorker;

    private ExecutorService es = Executors.newFixedThreadPool(500);

    @Test
    void testIdWorker() throws InterruptedException {
    
    

        CountDownLatch latch = new CountDownLatch(300);
        Runnable task = () -> {
    
    
            for (int i = 0; i < 100; i++) {
    
    
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
    
    
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time: "+(end - begin));
    }
}

idea:

image-20230131194319493

redis:

image-20230131194459587

Knowledge Tip: About countdownlatch

countdownlatch is called a signal gun: the main function is to synchronize and coordinate the waiting and wake-up problems in multi-threads

If we don't have CountDownLatch, then because the program is asynchronous, when the asynchronous program is not executed, the main thread has already been executed, and then we expect that after all the sub-threads are finished, the main thread will go again, so we need to use it at this time to CountDownLatch

There are two most important methods in CountDownLatch

1、countDown

2、await

The await method is a blocking method. We are worried that the main thread will execute first when the sub-threads are not executed, so using await can make the main thread block, so when will the main thread stop blocking? When the variable maintained inside CountDownLatch becomes 0, it will no longer block and let go directly. Then when the variable maintained by CountDownLatch becomes 0, we only need to call countDown once, and the internal variable will be reduced by 1. Let's divide the thread and variable Binding, after executing a sub-thread, a variable will be reduced. When all sub-threads are completed, the variable maintained by CountDownLatch is 0, and await will no longer block at this time, and the counted time is the time after all sub-threads are executed.

add coupon

Each store can issue coupons, which are divided into cheap coupons and special coupons. Cheap coupons can be purchased at will, while special coupons need to be snapped up:

image-20230131195950570

tb_voucher: Basic information of the coupon, discount amount, usage rules, etc.
tb_seckill_voucher: Coupon inventory, start time, and end time. This information is only required for special coupons

Since the discount is not very strong, the parity coupon can be claimed at will

Because of the strong discount, the vouchers have to limit the quantity like the second type of rolls. It can also be seen from the structure of the table that in addition to the basic information of the discount coupons, the special coupons also have inventory, snap-up time, end time, etc. and other fields

Add common volume code :

VoucherController:addVoucher

    @Resource
    private IVoucherService voucherService;

    /**
     * 新增普通券
     * @param voucher 优惠券信息
     * @return 优惠券id
     */
    @PostMapping
    public Result addVoucher(@RequestBody Voucher voucher) {
    
    
        voucherService.save(voucher);
        return Result.ok(voucher.getId());
    }

Added seckill volume code:

VoucherController:addSeckillVoucher

    /**
     * 新增秒杀券
     * @param voucher 优惠券信息,包含秒杀信息
     * @return 优惠券id
     */
    @PostMapping("seckill")
    public Result addSeckillVoucher(@RequestBody Voucher voucher) {
    
    
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }

VoucherServiceImpl

    //IVoucherService:void addSeckillVoucher(Voucher voucher);

	@Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
    
    
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
    }

test

request:http://localhost:8081/voucher/seckill

{
    
    
    "shopId": 1,
    "title": "100元代金券",
    "subTitle": "周一到周日均可使用",
    "rules": "全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食",
    "payValue": 8000,
    "actualValue": 10000,
    "type": 1,
    "stock": 100,
    "beginTime":"2023-01-31T20:40:00",
    "endTime":"2023-01-31T22:40:00"
}

Note : the units of payValue and actualValue are points, this is done to avoid decimals , 8000 points = 80 yuan, payValue / actualValue is 20% off

postman:

image-20230131210331884

mysql:

image-20230131210448889

Front page:

image-20230131210538143

Test passed!

Realize the second kill order

The core idea of ​​placing an order: when we click to buy, the request on the right will be triggered, and we only need to write the corresponding controller

image-20230131221341067

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.

image-20230131221417256

VoucherOrderServiceImpl

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDateTime;


@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
    

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    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
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 7.保存订单
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(orderId);
    }
}

test

Enter "688 Tea Restaurant" on the front-end page to snap up the flash coupons

  • The front-end page shows that the snap-up is successful
  • Insert the corresponding data into the database

Test passed!

image-20230131221013760

Analysis of Inventory Oversold Problem

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

The overselling problem is a typical multi-thread security problem. The common solution to this problem is to add locks: for locking, we usually have two solutions: see the figure below:

image-20230201133429486

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 lock: There will be a version number. Every time the data is operated, the version number will be +1. When submitting the data back, it will check whether it is 1 greater than the previous version. If it is greater than 1, the operation is successful. This mechanism The core logic is that if the version number is only 1 greater than the original during the operation, it means that no one has modified it during the operation, and his operation is safe. If it is not greater than 1, the data will be modified However, of course, there are some variants of optimistic locking, such as cas

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 the middle has not been modified, and at this time, the new value will be used to replace the memory value

Among them, do while is to perform 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;

How to use it in the course:

The method used in the course is that there is no spin operation like cas, and there is no +1 to the version number of the version. His operation logic is to perform +1 operation on the version number during the operation, and then ask the version if it is 1 Only under certain circumstances can it be operated, 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 also needs to add conditions at the end version =1, but now thread 1 has already operated, so thread 2 does not meet the condition of version=1 when operating, so thread 2 cannot execute successfully

image-20230201133541614

Test (unlocked): three pits

  1. Data recovery

    • tb_voucher_order: delete existing records
    • tb_seckill_voucher: stock changed to 100
  2. jmeter test

    • Open jmeter, import the flash purchase.jmx
  3. start up

    • The first pit: 401 Unauthorized

    • The second pit: the seckill has expired

    • The third pit: Get the null pointer of the login user id

       //Long id = UserHolder.getUser().getId();手动添加id
              voucherOrder.setUserId(666L);
      
  4. report error

    jmeter:

    image-20230201143637262

    postman:

    image-20230201143751140

  5. Modify the interceptor configuration (the first pit)

    • 401: Unauthorized meaning, similar to 403
    • Interceptor configuration: all methods under voucher-order are allowed

    image-20230201144047390

  6. restart test

    still error

    postman test:

    image-20230201145212762

    Modify the end time of the database flash coupon : omitted (the second pit)

  7. test again

    Then report an error

    image-20230201145926842

    Start Nginx, log in after opening the front-end page

  8. Still a null pointer exception (the third pit)

    • Since Threadlocal is used to save user information to prevent memory leaks, the interceptor is equipped to delete user information
    • In view of this, we manually write the id, just for testing

    image-20230201150730497

  9. restart test

    This time it finally worked! ! !

    mysql:

    image-20230201151221910

    jmeter:

    image-20230201151457443

    Result Description: Oversold

  10. test passed

    • This time it is mainly to build a test environment, step on the pit, and then it will be much smoother

Optimistic locking solves the oversold problem

Modify the code scheme one

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 person in 100 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

test

  1. jmeter error

    image-20230201152620350

  2. Data recovery

  3. test again

    jmeter

    image-20230201152815132

    mysql

    image-20230201153142538

  4. test passed

    • Although the abnormal rate is very high, there must be some people who grab the flash coupons, and it will not be 100% abnormal

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

Note : This can only guarantee that it will not be oversold, but there is no limit to the number of times each thread snaps up the flash coupons, that is, one person makes more orders

test

  1. Data recovery

  2. start test

    jmeter

    image-20230201154113418

    mysql: normal

    Omit (100 pieces of order data, inventory is 0)

  3. Test success!

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 will be returned

Illustration:

image-20230201154357530

Coupon flash sale - one person one order

Requirement: Modify the seckill business, require the same coupon, and a user can only place an order

Now the problem is:

Coupons are for drainage, but the current situation is that one person can grab this coupon without limit, so we should add a layer of logic so that a user can only place one order instead of allowing one user to place multiple orders

The specific operation logic is as follows: For example, whether the time is sufficient, if the time is sufficient, further judge whether the stock is sufficient, and then check whether the order has been placed according to the coupon id and user id, if the order has been placed, no more orders will be placed, Otherwise place an order

image-20230201161411477

Preliminary code: add one person one order logic

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;


@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
    

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    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.一人一单逻辑
        // 5.1.用户ID
        Long userId = UserHolder.getUser().getId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2判断是否存在
        if (count > 0){
    
    
            //用户已经购买过了
            return Result.fail("用户已经购买过一次了");
        }
        // 6.扣减库存
//        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 = ?
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).gt("stock",0).update(); //where id = ? and stock > 0
        if (!success){
    
    
            //扣减库存
            return Result.fail("库存不足!");
        }
        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 8.保存订单
        save(voucherOrder);
        // 9.返回订单id
        return Result.ok(orderId);
    }
}

test

First correct the previous test method: the wrong way of writing the login user ID to death, since I have never used jmeter before, I don’t know that token can be passed

jmeter:

image-20230201161917061

browser:

image-20230201162104167

Comments on Dark Horse: The teacher in the SMS login section said that when the front-end page sends data to the back-end, authorization is carried. For those who forgot, you can read my previous notes.

Change the value corresponding to authorization in the jmeter login status header to the token value in the browser, so that each thread request will carry the login user information (the same user), and test one person one order

test preliminary code

  1. restore data (inventory 100, order 0)

  2. jmeter test

    image-20230201162906330

    mysql:

    image-20230201163039445

  3. Test success

    • Normal situation: Stock: 99, Orders: 1
    • Exception: as above
    • Problem: oversold

synchronized lock

Existing problems : The current problem is still the same as before. When sending and querying the database, there is no order, so we still need to add locks, but optimistic locking is more suitable for updating data, and now it is inserting data, so we need to use pessimistic locking operations

Note : There are a lot of problems mentioned here. We need to think slowly. First of all, our initial solution is to encapsulate a createVoucherOrder method. At the same time, in order to ensure its thread safety, a synchronized lock is added to the method.

Base

Which part of the code is locked?

  • checking order
  • Determine whether the order exists
  • new order

Encapsulate the above code into a single method, lock and transaction

Error locking:

  • Add a lock to the method, the lock is this, which refers to the current object, and the lock on the method will cause the thread to execute serially, and the performance will be greatly reduced

  • Take a chestnut:

  • Zhang San and Li Si visit, so locking will cause Zhang San to execute Li Si before he can execute

  • What we want is: lock a single user, a user can only place an order, Zhang San and Li Si can execute in parallel, as long as Zhang San and Li Si can only place an order

lock correctly

  • The locked one should be the current user

  • Use the user id to lock, and the scope of the lock becomes smaller

  • The same user, add the same lock, different users, add different locks

The following code locks for errors

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
    
    

	Long userId = UserHolder.getUser().getId();
         // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
    
    
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
    
    
            // 扣减失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
}

Advanced

Adding locks in this way, the granularity of the lock is too coarse. In the process of using the lock, it is very important to control the granularity of the lock , because if the granularity of the lock is too large, every thread will be locked when it comes in, so we need to control it. The granularity of the lock, the above code needs to be modified

synchronized(userId.toString())

Expectation: the same userId value means the same lock

Every time the userId is requested, it is a different object. If the object changes, the lock will change. We require the same value, so toString()

View toString() source code:

Every time a new object is new, even if the userId is the same, userId.toString() is different

image-20230201195349902

intern() This method is to get data from the constant pool. If we use userId.toString() directly, the object he gets is actually a different object. For the new object, we must ensure that the lock must be the same when using the lock , so we need to use the intern() method

image-20230201195848351

@Transactional
public  Result createVoucherOrder(Long voucherId) {
    
    
	Long userId = UserHolder.getUser().getId();
    //synchronized(userId.toString()){
    
    
	synchronized(userId.toString().intern()){
    
    
         // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
    
    
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
    
    
            // 扣减失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    }
}

advanced

There is still a problem with the above code. The reason for the problem is that the current method is controlled by the spring transaction. If you add a lock inside the method, the current method transaction may not be committed, but the lock has been released, which will also cause problems, so we choose to use the current The method is wrapped up as a whole to ensure that there will be no problems with the transaction: as follows:

In the seckillVoucher method, add the following logic, so that the characteristics of the transaction can be guaranteed, and the granularity of the lock can also be controlled

Requirements: acquire the lock first, then commit the transaction, and finally release the lock

image-20230201200913904

But the above method still has problems, because the method you call is actually called by this. If the transaction wants to take effect, you have to use a proxy to take effect, so in this place, we need to obtain the original transaction object to operate the transaction

image-20230201201554074

full code

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
    

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @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("库存不足!");
        }

        Long userId = UserHolder.getUser().getId();
        synchronized(userId.toString().intern()) {
    
    
            // 获取代理对象 事务
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }
    @Transactional
    public Result createVoucherOrder(Long voucherId){
    
    

        // 5.一人一单逻辑
        Long userId = UserHolder.getUser().getId();

            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2判断是否存在
            if (count > 0) {
    
    
                //用户已经购买过了
                return Result.fail("用户已经购买过一次了");
            }
            // 6.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock= stock -1")
                    .eq("voucher_id", voucherId).gt("stock", 0).update(); //where id = ? and stock > 0
            if (!success) {
    
    
                //扣减库存
                return Result.fail("库存不足!");
            }
            // 7.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 7.1.订单id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 7.2.用户id
            Long id = UserHolder.getUser().getId();
            voucherOrder.setUserId(id);
            //7.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            // 8.保存订单
            save(voucherOrder);

            return Result.ok(orderId);

    }
}

pom.xml

        <--!>添加动态代理</--!>
		<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

startup class

//暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)

Need to master: dynamic proxy, AOP, transaction, pessimistic lock, lock range, etc.

knowledge tips

  • Spring framework transaction failure
  • aop proxy object
  • synchronized lock object

test

  1. restore database data

  2. restart test

    jmeter

    image-20230201203106961

    mysql

    image-20230201203307302

  3. Successfully locked!

Concurrency issues in a cluster environment

Locking can solve the one-person-one-one security problem in the case of a single machine, but it will not work in the cluster mode.

  1. We start two copies of the service, the ports are 8081 and 8082:

    image-20230201212908670

  2. Then modify the nginx.conf file in the nginx conf directory to configure reverse proxy and load balancing:

    image-20230201213109264

  3. Let the Nginx configuration file take effect

    image-20230201213218165

  4. Test whether the configuration is successful

    • Browser input: http://localhost:8888/api/voucher/list/1

    • Refresh twice: see 8001 service and 8003 service once

    • The configuration is successful!

  5. test lock

    • Data recovery
    • Too lazy to copy authorization, use jmeter test
  6. jmeter configuration

    image-20230201214358205

  7. test

    jmeter

    image-20230201214233754

    mysql

    image-20230201214516487

  8. Test success

After many tests, the lock does not necessarily fail, and there are normal situations! ! !

Cause analysis of lock failure

Now that we have deployed multiple tomcats, and each tomcat has its own jvm, then suppose there are two threads inside the tomcat of server A. Since these two threads use the same code, their locks The objects are the same, and mutual exclusion can be achieved, but if there are two threads inside the tomcat of server B, their lock objects are written the same as those of server A, but the lock objects are not the same, so Thread 3 and thread 4 can achieve mutual exclusion, but they cannot achieve mutual exclusion with thread 1 and thread 2. This is why the syn lock fails in a cluster environment. In this case, we need to use distributed locks to solve this problem.

image-20230201210255771

Summarize

  1. Basic implementation of seckill coupons

  2. oversold problem

  3. Solve the oversold problem based on optimistic locking (optimistic locking and pessimistic locking)

  4. Seckill's one-person-one-order restriction function

  5. Realize the one-person-one-order limit of seckill

  6. Thread Safety Issues in Standalone Mode

  7. Thread Safety Issues in Cluster Mode

Synchronized lock and Lock lock :

CountDownLatch

Pessimistic locking and optimistic locking :

Guess you like

Origin blog.csdn.net/Htupc/article/details/128960067