(4) Practical combat of oversold inventory cases – optimizing redis distributed locks

Foreword

In the previous section, we have implemented the use of redis distributed locks to solve the problem of “oversold” products. This section is about the optimization of redis distributed locks. In the redis distributed lock in the previous section, our lock has two issues that can be optimized. First, the lock needs to be reentrant, so the same thread does not need to acquire the lock repeatedly; second, the lock does not have a renewal function, resulting in the lock being released before the business is completed, and there are certain concurrent access problems. In this case, the reentrant lock is implemented by using the hash data structure of redis, and the timer is used to implement the lock renewal function to complete the optimization of the redis distributed lock. Finally, we integrated the third-party redisson toolkit to complete the optimization of the above two points of distributed locks. Redisson provides a simple and easy-to-use API, allowing developers to easily use Redis in a distributed environment.

Text

  • Locked Lua script: Use the exists and hexists instructions to determine whether a lock exists. If it does not exist or a lock exists and the field under the lock has a value, use the hincrby instruction to increase the value of the lock by 1 to achieve It can be reentrant, otherwise it returns 0 directly and the lock fails.
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end"
  • Unlocking Lua script: Use the hexists instruction to determine whether there is a lock. If it is 0, it means that there is no lock corresponding to the field field, and nil will be returned directly; if the hincrby instruction is used to reduce the value of the lock field field by 1, If the value is 0, it means that the lock is no longer occupied and the lock can be deleted; otherwise, 0 is returned directly, which means that the lock is reentrant and the lock has not been released.
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
"then " +
" return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end"
  • Lua script to implement renewal: Use the hexists instruction to determine whether the field value of the lock exists. If the value is 1, update the expiration time of the lock. Otherwise, return 0 directly, indicating that the lock is not found. , the renewal failed.
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
  • Create a custom lock tool class MyRedisDistributeLock to implement locking, unlocking and renewal functions

– MyRedisDistributeLock implementation

package com.ht.atp.plat.util;

import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class MyRedisDistributeLock implements Lock {

    public MyRedisDistributeLock(StringRedisTemplate redisTemplate, String lockName, long expire) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.expire = expire;
        this.uuid = getId();
    }

    /**
     * redis tool class
     */
    private StringRedisTemplate redisTemplate;


    /**
     * Lock name
     */
    private String lockName;

    /**
     * Expiration
     */
    private Long expire;

    /**
     * Lock value
     */
    private String uuid;

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() {

    }

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        if (time != -1) {
            this.expire = unit.toSeconds(time);
        }
        String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
                " redis.call('expire', KEYS[1], ARGV[2]) " +
                " return 1 " +
                "else " +
                " return 0 " +
                "end";
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
            Thread.sleep(50);
        }
// //After successful locking, automatic renewal
        this.renewExpire();
        return true;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
                "then " +
                " return nil " +
                "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
                "then " +
                " return redis.call('del', KEYS[1]) " +
                "else " +
                " return 0 " +
                "end";
        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        if (flag == null) {
            throw new IllegalMonitorStateException("this lock doesn't belong to you!");
        }
    }

    @NotNull
    @Override
    public Condition newCondition() {
        return null;
    }

    /**
     * Give thread splicing a unique identifier
     *
     * @return
     */
    private String getId() {
        return UUID.randomUUID() + "-" + Thread.currentThread().getId();
    }


    private void renewExpire() {
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                " return redis.call('expire', KEYS[1], ARGV[2]) " +
                "else " +
                " return 0 " +
                "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("-------------------");
                Boolean flag = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire));
                if (flag) {
                    renewExpire();
                }
            }
        }, this.expire * 1000 / 3);
    }
}

– Implement locking function

– Implement unlocking function


– Use Timer to implement lock renewal function

  • Use MyRedisDistributeLock to implement inventory locking business

– Use custom MyRedisDistributeLock tool class to implement locking business

public void checkAndReduceStock() {
        //1. Get the lock
        MyRedisDistributeLock myRedisDistributeLock = new MyRedisDistributeLock(stringRedisTemplate, "stock", 10);
        myRedisDistributeLock.lock();

        try {
            // 2. Query inventory quantity
            String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");
            // 3. Determine whether the inventory is sufficient
            if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
                Integer quantity = Integer.valueOf(stockQuantity);
                if (quantity > 0) {
                    // 4. Deduct inventory
                    stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                }
            } else {
                System.out.println("This inventory does not exist!");
            }
        } finally {
            myRedisDistributeLock.unlock();
        }
    }

– Start services 7000, 7001, and 7002, and stress test the optimized custom distributed lock: the average access time is 362ms, the throughput is 246 per second, and the inventory deduction is 0, indicating that the optimized distributed lock is available.

  • Integrate the redisson toolkit, use third-party toolkits to implement distributed locks, and complete concurrent access “oversold” problem case demonstration
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.6</version>
</dependency>
  • Create a redisson configuration class and introduce the redisson client tool
package com.ht.atp.plat.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class MyRedissonConfig {

    @Bean
    RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.110.88:6379");
        //Configure the default timeout of the watchdog to 30s for renewal use
        config.setLockWatchdogTimeout(30000);
        return Redisson.create(config);
    }
}
  • Use Redisson lockImplement the “oversold” business method
//Reentrant lock
    @Override
    public void checkAndReduceStock() {
        // 1. Lock, retry if acquisition of lock fails
        RLock lock = this.redissonClient.getLock("lock");
        lock.lock();

        try {
            // 2. Query inventory quantity
            String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");
            // 3. Determine whether the inventory is sufficient
            if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) {
                Integer quantity = Integer.valueOf(stockQuantity);
                if (quantity > 0) {
                    // 4. Deduct inventory
                    stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                }
            } else {
                System.out.println("This inventory does not exist!");
            }
        } finally {
            // 4. Release the lock
            lock.unlock();
        }
    }
  • Enable 7000, 7001, and 7002 services, pressure test inventory deduction interface

– Stress test results: average access time 222ms, throughput 384 times per second

– The inventory deduction result is 0

Conclusion

In summary, whether you customize distributed locks or use the redisson tool class, distributed locks can be implemented to solve the “oversold problem” of concurrent access. The integration of the redisson tool is more convenient and concise. It is recommended to use the redisson toolkit. This section ends here, see you next time. . . . . .