Distributed lock -- realizing distributed lock based on Database

Keywords: Database SQL MySQL Spring

In the last blog, I simply said what is distributed lock, and built a basic environment (very simple). This blog needs to start to experience distributed lock formally. Because it is developed on a single machine, there is no cluster, but the specific implementation of the code method is no different from that of the cluster, and only through JMeter to simulate multi threads to achieve high concurrency.

Simulate business scenarios

1. SQL of goods inventory sales in simulation database

  <!--Update stock-->
  <update id="updateStock" parameterType="com.learn.lockmodel.entity.ProductLock">
    update product_lock
    set stock = stock - #{stock,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
  </update>

2. Entity selling goods

The BindingResult is the verification result. As mentioned before, the transfer gate: Parameter verification in spring boot BaseResponse is the encapsulated unified message processing object. The specific code is as follows:

@Data
public class BaseResponse<T> {

    private Integer code;
    private String msg;
    private T data;

    public BaseResponse(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    public BaseResponse(StatusCode statusCode, T data) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = data;
    }

    public BaseResponse(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public BaseResponse(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

5. postman to verify

Commodity id: 10010, commodity inventory: 1000

Use postman to build the following request

After the request is sent successfully, you will see a normal decrease in inventory

JMeter simulation high concurrency

JMeter, in my opinion, can simulate all the scenarios used in our development, and its functions seem to be much more powerful than postman, About Apache JMeter Download address: Download addresses of JMeter versions

install

Installation is an Easy to explode thing. After decompression, enter bin directory, click jmeter.bat batch script (in windows environment, in linux environment, click jmeter.sh), and JMeter can be started

Join HTTP header Manager

Click back - > Add - > configure component - > HTTP header manager.

After that, the data type of HTTP needs to be added to the HTTP header

This is different from postman, which is more convenient to add.

Add HTTP request header

Right click - > Add - > Sampler - > HTTP request

After joining the HTTP request, you need to configure the url, port and other parameters.

The request parameters here, the dynamic configuration we adopted, JMeter can automatically obtain the data in the specified file through the dynamic configuration variables, which requires adding the corresponding data file.

Add test data file

Right click - > Add - > configure component
After adding the file, we need to specify the CSV file and the variable name:

That's why the ${stock} variable is specified in the HTTP request. Here we specify two data in the data CSV file - 2,4.

Set thread group

Forget to mention that when JMeter is opened, there will be a test plan by default. After adding thread groups to the test plan, you can add HTTP information management headers, HTTP request headers and test data files.

Set related thread groups:

Initiate request

After the request is initiated and 1000 threads are processed, we will find the following situations

It's intolerable that the inventory becomes negative (although it can only be regarded as a simple simulation of multithreading to some extent, if these codes are placed on multiple servers and executed in multiple JVM s, negative inventory will still occur).

Optimistic lock and pessimistic lock

The introduction of optimistic lock and pessimistic lock will not be repeated here. Optimistic lock introduces the concept of version number. If the version number is inconsistent, it means that other operations have modified it, and the data becomes dirty data. Pessimistic lock is nothing more than queuing all requests. These basic concepts have been summed up by Daniel in place, Baidu search a lot, here is a link directly attached. An introduction to optimistic lock and pessimistic lock

Database based implementation

The database is the data source of the application program. The access restriction of the data is completed in the database stage, and the operation of the distributed lock can be realized naturally.

The realization of optimistic lock

After the version number field is introduced, whether the update matches the version number with this thread will be updated. If it is consistent, the data will be updated. If it is inconsistent, the update will be discarded. On the basis of basic code, we only need to modify SQL to implement.

<!--Update stock-Optimistic lock v1-->
<update id="updateStockV1" parameterType="com.learn.lockmodel.entity.ProductLock">
  update product_lock set stock = stock - #{stock,jdbcType=INTEGER},version=version+1
  where id = #{id,jdbcType=INTEGER} and version = #{version,jdbcType=INTEGER} and stock > 0
</update>

The version judgment is added to the where condition, and the version number is + 1 when updating.

There is no need to change the business code, just call the specified data access code.

@RequestMapping(value=prefix+"/db/update/optimistic",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBaseOptimisticLock(@RequestBody @Validated ProductLockDto productLockDto,BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    BaseResponse result = new BaseResponse(StatusCode.Ok);
    try{
        log.debug("Current request data:{}",productLockDto);
        int res = dataLockService.updateStockWithOptimisticLock(productLockDto);
        if(res<=0){//If database level update fails, direct purchase fails
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("Failed to update commodity inventory, exception information is:{}",e.fillInStackTrace());
        result = new BaseResponse(StatusCode.Fail);
    }
    return result;
}

test

In order to facilitate the calculation, we change all the purchases to 1, and then launch multiple threads for stress testing.

Concurrent 2000 threads, initialization inventory 50000, start JMeter test. If version number + inventory = 50000, data is normal (this formula can be used for verification only when the number of purchases is 1, and data is correct only when the formula is established at any time).

The test results are as follows

The realization of pessimistic lock

Let'S go back to the source. When data is illogical, it'S actually dirty reading. If we use pessimistic lock, we need to add X lock when reading data. For database X lock and S lock, please refer to this blog—— MySql (3) - transactions and locks

Add X lock

Using for update statement, add X lock to the database when reading, so as to avoid dirty reading of data.

  <!--Query by primary key for update Pessimistic lock-->
  <select id="selectByPKForNegative" resultType="com.learn.lockmodel.entity.ProductLock">
    SELECT <include refid="Base_Column_List"/> FROM product_lock
    WHERE id=#{id} FOR UPDATE
  </select>

Add the for update statement to join the X lock.

There are still few changes to be made at the business code level, only the logic of data query needs to be changed. As follows:

/*
* Update operation of pessimistic lock
 * @param dto
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public int updateStockNegativeLock(ProductLockDto dto){
    int res = 0;
    //When obtaining inventory data, add X lock
    ProductLock negativeLockEntity = lockMapper.selectByPKForNegative(dto.getId());
    if(negativeLockEntity!=null && negativeLockEntity.getStock().compareTo(dto.getStock())>=0){
        negativeLockEntity.setStock(dto.getStock());
        res = lockMapper.updateStockForNegative(negativeLockEntity);
        if(res>0){//Rush to buy successfully
            log.info("Successfully placed the order,stock{}",negativeLockEntity.getStock());
        }else{
            log.error("Panic buying failed");
        }
        return res;
    }
    return res;
}

Example of controller:

 /*
 * Pessimistic lock update database
 * @param productLockDto
 * @param bindingResult
 * @return
 */
@RequestMapping(value=prefix+"/db/update/negative",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBaseNegativeLock(@RequestBody @Validated ProductLockDto productLockDto,BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    BaseResponse result = new BaseResponse(StatusCode.Ok);
    try{
        log.debug("Current request data:{}",productLockDto);
        int res = dataLockService.updateStockNegativeLock(productLockDto);
        if(res<=0){//If database level update fails, direct purchase fails
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("Failed to update commodity inventory, exception information is:{}",e.fillInStackTrace());
        result = new BaseResponse(StatusCode.Fail);
    }
    return result;
}

**Logically, this kind of lock is directly loaded into MySQL. When each request fails to update data, it will wait until the data is updated successfully. Therefore, as long as the number of database connections is sufficient, there will not be many update failures like optimistic locks. **The test results are as follows:

summary

This blog introduces optimistic lock and pessimistic lock at database level.

122 original articles published, 35 praised, 80000 visitors+
Private letter follow

Posted by soulrazer on Sun, 26 Jan 2020 02:06:59 -0800