(3) Actual combat of inventory oversold cases – using redis distributed locks to solve the “oversold” problem

Foreword

In the previous section, we introduced how to use traditional locks (row locks, optimistic locks, pessimistic locks) of the MySQL database to solve the “oversold problem” caused by concurrent access. Although mysql’s traditional lock can solve the problem of concurrent access very well, in terms of performance, mysql’s performance does not seem to be that good, and it will be subject to single points of failure. In this section, we introduce a solution with better performance, using the in-memory database redis to implement distributed locks to control the “overselling” problem caused by concurrent access. There is no introduction to the establishment of the redis environment here. You can check the author’s previous blog content.

Text

  • Add redis dependencies and configuration information to the project

– pom dependency configuration

<!-- Database connection pool toolkit -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!--redis launcher-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

– application.yml configuration

spring:
  application:
    name: ht-atp-plat
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.110.88:3306/ht-atp?characterEncoding=utf-8 & serverTimezone=GMT+8 & useAffectedRows=true & nullCatalogMeansCurrent=true
    username: root
    password: root
  profiles:
    active:dev
  # redis configuration
  redis:
    host: 192.168.110.88
    lettuce:
      pool:
        #The maximum number of connections in the connection pool (use a negative value to indicate no limit), the default is 8
        max-active: 8
        # The minimum idle connection in the connection pool defaults to 0
        min-idle: 1
        # The maximum blocking waiting time of the connection pool (use a negative value to indicate no limit), the default is -1
        max-wait: 1000
        #The maximum idle connection in the connection pool defaults to 8
        max-idle: 8

– redis serialization configuration

package com.ht.atp.plat.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    /**
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        //Cache serialization configuration to avoid storing garbled characters
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key adopts String serialization method
        template.setKeySerializer(stringRedisSerializer);
        // The hash key also uses String serialization method
        template.setHashKeySerializer(stringRedisSerializer);
        //The value serialization method uses jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // The value serialization method of hash uses Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
  • Add the inventory quantity of product P0001 to 10000 in redis

  • Business test without locking using redis

– Business test code

 /**
     * Use redis without locking
     */
    @Override
    public void checkAndReduceStock() {
        // 1. Query inventory quantity
        String stockQuantity = redisTemplate.opsForValue().get("P0001").toString();

        // 2. Determine whether the inventory is sufficient
        if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
            Integer quantity = Integer.valueOf(stockQuantity);
            if (quantity > 0) {
                // 3. Deduct inventory
                redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
            }
        }
    }

– Use jmeter stress test to check the test results: the inventory has not been reduced to 0, indicating that there is an “oversold” problem.

  • Use the setnx command of redis to lock, open three identical services, and use jmeter to perform stress testing

– redis lock test code

/**
     * Use redis to lock
     *
     */
    @Override
    public void checkAndReduceStock() {
        // 1. Use setnx to lock
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000");
        // 2. Retry: recursive call, if the lock cannot be obtained
        if (!lock) {
            try {
                //pause 50ms
                Thread.sleep(50);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. Query inventory quantity
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. Determine whether the inventory is sufficient
                if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5. Deduct inventory
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("This inventory does not exist!");
                }
            } finally {
                // 5. Unlock
                redisTemplate.delete("lock-stock");
            }
        }
    }

– Start services 7000, 7001, 7002

– jmeter Stress test results: average access time 364ms, interface throughput 249 per second

– The redis database inventory result is: 0, the concurrent “oversold” problem is solved

  • The above common locking methods have deadlock problems and solutions to the deadlock problems

– Causes of deadlock: Under normal circumstances, the above-mentioned redis locking can solve the problem of concurrent access, but there are also deadlock problems. For example, after the 7000 service obtains the lock, the lock is not released due to a service exception. Then the 7001 and 7002 services will never be able to obtain the lock.

– Solution: Set expiration time for the lock and automatically release the lock

① Use expire to set the expiration time (lack of atomicity: if an exception occurs between setnx and expire, the lock cannot be released)

② Use the setex command to set the expiration time: set key value ex 3 nx (to ensure that the atomic operation not only achieves the effect of setnx, but also sets the expiration time)

– Code

public void checkAndReduceStock() {
        // 1. Use setex to lock to ensure the atomicity of the lock and the lock can be automatically released
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000",3, TimeUnit.SECONDS);
        // 2. Retry: recursive call, if the lock cannot be obtained
        if (!lock) {
            try {
                //pause 50ms
                Thread.sleep(50);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. Query inventory quantity
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. Determine whether the inventory is sufficient
                if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5. Deduct inventory
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("This inventory does not exist!");
                }
            } finally {
                // 5. Unlock
                redisTemplate.delete("lock-stock");
            }
        }
    }

– Test result: Inventory deduction is 0 and the lock is released

  • Prevent accidental deletion. In the above ordinary locking method, the lock may be accidentally deleted

– The reason why the lock is accidentally deleted: In the above locking scenario, the following situation will occur. After request method A acquires the lock, the lock is automatically released before the business is completed. At this time, request method B will also acquire the lock. When the lock is reached, before B business is completed, A completes the execution and performs manual deletion of the lock operation. At this time, the lock of B business will be released, resulting in B being released just after acquiring the lock, resulting in subsequent concurrent access problems. .

– Concurrency issues caused by accidental deletion of simulation locks

– Inventory deduction result: no deduction is 0, resulting in concurrency problems

– Solution: Each request uses a globally unique UUID as the value. Before deleting the lock, first determine whether the value is the same, and then delete the lock if it is the same.

public void checkAndReduceStock() {
        // 1. Use setex to lock to ensure the atomicity of the lock and the lock can be automatically released
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS);
        // 2. Retry: recursive call, if the lock cannot be obtained
        if (!lock) {
            try {
                //pause 50ms
                Thread.sleep(10);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. Query inventory quantity
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. Determine whether the inventory is sufficient
                if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5. Deduct inventory
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("This inventory does not exist!");
                }
            } finally {
                // 5. First determine whether it is your own lock, and then unlock it
                String redisUuid = (String) redisTemplate.opsForValue().get("lock-stock");
                if (StringUtils.equals(uuid, redisUuid)) {
                    redisTemplate.delete("lock-stock");
                }
            }
        }
    }

– Existing problems: Since the operations of judging locks and unlocking are not atomic, there will still be accidental deletion operations. For example, when A requests to delete the lock after completing the judgment, A’s lock is automatically released at this time, and B requests to obtain it. lock. At this time, request A will manually delete the lock requested by B, and the problem of concurrent access still exists. The probability is very small.

  • Use Lua script to solve the problem of manual lock release and deletion, which is an atomic operation

– Lua code to solve accidental deletion operation

public void checkAndReduceStock() {
        // 1. Use setex to lock to ensure the atomicity of the lock and the lock can be automatically released
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS);
        // 2. Retry: recursive call, if the lock cannot be obtained
        if (!lock) {
            try {
                //pause 50ms
                Thread.sleep(10);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. Query inventory quantity
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. Determine whether the inventory is sufficient
                if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5. Deduct inventory
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("This inventory does not exist!");
                }
            } finally {
                // 5. First determine whether it is your own lock, and then unlock it
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                        "then " +
                        " return redis.call('del', KEYS[1]) " +
                        "else " +
                        " return 0 " +
                        "end";
                redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock-stock"), uuid);
            }
        }
    }

Conclusion

The content about using redis distributed locks to solve the “oversold” problem ends here. See you in the next issue. . . . . .

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Cloud native entry-level skills treeHomepageOverview 16,750 people are learning the system