Redis distributed lock implementation and error case analysis

Keywords: Jedis Redis Spring less

Recently, I looked at the knowledge of distributed locks. They are still used more in practice, because in the case of high concurrency, if they are not suitable for distributed locks, the data will definitely have problems, such as the problem of seconds killing commodity inventory in e-commerce platform.

The reasons for choosing redis are as follows:

1. Redis has very high performance.

2. Redis itself is a single thread, so there is no concurrency problem.

3. The Redis command supports this well and is easy to implement.

I don't need to say much, just look at the code. I've won the bid in the code annotation for some details and explanations.

Add the official recommended redis connection client jedis in pom.xml file

pom.xml

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>

 

Reids configuration entity class

package com.lds.springbootdemo.redis;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @program: core
 * @description: redis Connection configuration entity
 * @author: lidongsheng
 * @createData: 2020-03-13 11:46
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 11:46
 * @updateContent: redis Connection configuration entity
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.timeout}")
    private int timeout;
    //maximum connection
    @Value("${spring.redis.lettuce.pool.max-active}")
    private int poolMaxActive;
    //Maximum number of waiting connections
    @Value("${spring.redis.lettuce.pool.max-idle}")
    private int poolMaxIdle;
    //Maximum connection waiting time ms
    @Value("${spring.redis.lettuce.pool.max-wait}")
    private int poolMaxWait;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public int getPoolMaxActive() {
        return poolMaxActive;
    }

    public void setPoolMaxActive(int poolMaxActive) {
        this.poolMaxActive = poolMaxActive;
    }

    public int getPoolMaxIdle() {
        return poolMaxIdle;
    }

    public void setPoolMaxIdle(int poolMaxIdle) {
        this.poolMaxIdle = poolMaxIdle;
    }

    public int getPoolMaxWait() {
        return poolMaxWait;
    }

    public void setPoolMaxWait(int poolMaxWait) {
        this.poolMaxWait = poolMaxWait;
    }
}

 

Add the connection pool factory class of Jedis

package com.lds.springbootdemo.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @program: core
 * @description: Jedis Connection pool
 * @author: lidongsheng
 * @createData: 2020-03-13 12:03
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 12:03
 * @updateContent: Jedis Connection pool
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */
@Component
public class RedisPoolFactory {

    @Autowired
    private RedisConfig redisConfig;

    @Bean
    public JedisPool jedisPoolFactory() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
        poolConfig.setMaxTotal(redisConfig.getPoolMaxActive());
        poolConfig.setTestOnBorrow(true);
        poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait());
        JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
                redisConfig.getTimeout(), redisConfig.getPassword(), 0);
        return jp;
    }
}

In order to facilitate data management in redis, a unified prefix class of key is added, and the prefix under a module can be unified. This item is not allowed

package com.lds.springbootdemo.redis;

/**
 * @program: core
 * @description: key Prefix class (in order to facilitate data management in redis, a prefix class is added, and the prefix under a module can be unified)
 * @author: lidongsheng
 * @createData: 2020-03-13 15:39
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 15:39
 * @updateContent: key Prefix class
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */

public interface KeyPrefix {
    Long expireMillisecond();
    String getPrefix();
}
package com.lds.springbootdemo.redis.keyPrefix;


import com.lds.springbootdemo.redis.KeyPrefix;

/**
 * @program: core
 * @description: Abstract key prefix class, all theories need to be inherited
 * @author: lidongsheng
 * @createData: 2020-03-13 15:40
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 15:40
 * @updateContent:
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */

public abstract class BasePrefix implements KeyPrefix {

    private Long expireMillisecond;

    private String prefix;

    public BasePrefix(Long expireMillisecond, String prefix) {
        this.expireMillisecond = expireMillisecond;
        this.prefix = prefix;
    }

    public BasePrefix(String prefix) {
        this(0L, prefix);
    }

    @Override
    public Long expireMillisecond() {
        return expireMillisecond;
    }

    @Override
    public String getPrefix() {
        String className = getClass().getSimpleName();
        return className + ":" + prefix;
    }

}

 

For example, in a series of redis operations of commodity sales, we can create a consistency key prefix class of commodity sales class

package com.lds.springbootdemo.redis.keyPrefix;

/**
 * @program: core
 * @description: Consistency key prefix of commodity sales class
 * @author: lidongsheng
 * @createData: 2020-03-13 15:46
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 15:46
 * @updateContent: redis
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */

public class GoodsoldPrefix extends BasePrefix {
    public GoodsoldPrefix(Long expireMillisecond, String prefix) {
        super(expireMillisecond, prefix);
    }

    public GoodsoldPrefix(String prefix) {
        super(prefix);
    }
}

 

Next, the key is to implement the distributed lock. I've explained everything in the comments.

package com.lds.springbootdemo.redis;

import com.alibaba.druid.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;


/**
 * @program: core
 * @description:
 * @author: lidongsheng
 * @createData: 2020-03-13 12:53
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 12:53
 * @updateContent:
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */
@Component
public class JedisUtil {

    private static JedisPool jedisPool;

    private static Logger logger = LoggerFactory.getLogger(JedisUtil.class);

    @Autowired
    public void setJedisPool(JedisPool jedisPool) {
        JedisUtil.jedisPool = jedisPool;
    }


    /**
     * Get lock
     *
     * @param keyPrefix       In order to facilitate the management of data in redis, a prefix class is added (under one module, the prefix can be unified, which is convenient for management and can also be removed)
     * @param key             All client key s are consistent
     * @param value           Expiration time stamp of the lock (use expiration time stamp to prevent one node from blocking and releasing the lock, and other nodes from obtaining the lock)
     * @param lockWaitTimeOut Wait timeout of acquiring lock in milliseconds (to prevent a large number of requests from continuously acquiring lock resources)
     * @return
     */
    public static boolean lock(KeyPrefix keyPrefix, String key, String value, Long lockWaitTimeOut) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //All key s are the same
            String realKey = keyPrefix.getPrefix() + key;
            //Get the timeout stamp of the lock
            Long deadTime = System.currentTimeMillis() + lockWaitTimeOut;
            //for loop is to try to acquire lock again and again in lockWaitTimeOut in case of failure to acquire lock
            for (; ; ) {
                //setnx is a string whose key is val when and only when the key does not exist. It returns 1. If the key exists, it does nothing and returns 0.
                //At this time, we can judge whether the lock is successful according to the return value of setNx. The return value of 1 indicates that no one owns the lock, and the return value of 0 indicates that someone else is holding the lock
                if (jedis.setnx(realKey, value) == 1) {
                    return true;
                }
                String currentValue = jedis.get(realKey);
                //Judge whether the current lock owned by other clients is expired
                if (!StringUtils.isEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) {
                    //If the lock owned by other clients expires, it will enter the method

                    //The getSet method sets the new value and returns the old value
                    //So this oldValue is the value of the previously expired lock
                    String oldValue = jedis.getSet(realKey, value);
                    //If oldValue==currentValue, the lock is obtained successfully
                    //Why add an oldValue==currentValue is to prevent multiple clients from obtaining locks at the same time in the case of high concurrency
                    //Because in the case of high concurrency, without value verification, it is likely that multiple clients will get locks when they execute at the same time.
                    //Therefore, it is necessary to judge the consistency of value. Because the value returned after only one client getSet operation must be equal to the currentValue
                    if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                        return true;
                    }
                }
                lockWaitTimeOut = deadTime - System.currentTimeMillis();
                //If the lock is not obtained successfully within the lockWaitTimeOut time, false will be returned. In the case of high concurrency, resources cannot be occupied all the time.
                if (lockWaitTimeOut <= 0L) {
                    return false;
                }
            }

        } catch (Exception ex) {
            logger.error(ex.getMessage());
            return false;
        } finally {
            returnResource(jedis);
        }
    }

    /**
     * Release lock
     *
     * @param keyPrefix
     * @param key
     * @param value
     */
    public static void unLock(KeyPrefix keyPrefix, String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = keyPrefix.getPrefix() + key;
            String currentValue = jedis.get(realKey);
            /* Compare the value of releasing lock with that of redis to prevent releasing locks of other clients
             * For example, when client A obtains the lock, the execution time of client A is relatively long, exceeding the expiration time of the lock, then B or C can obtain the lock
             * B After getting the lock, execute the business logic of B. during the execution of the business logic of B, A will not block any more at this time. After the execution, release the lock. If you do not compare value and release the lock directly, the lock of B will be released, so this is certainly incorrect.
             * So you have to compare value s
             */
            if (!StringUtils.isEmpty(currentValue)
                    && value.equals(currentValue)) {
                jedis.del(realKey);
            }
        } catch (Exception ex) {

        } finally {
            returnResource(jedis);
        }
    }

    /**
     * Return the resources of the connection pool to the connection pool
     *
     * @param jedis
     */
    public static void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

    /**
     * Add key value pair
     *
     * @param keyPrefix
     * @param key
     * @param value
     */
    public static void set(KeyPrefix keyPrefix, String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = keyPrefix.getPrefix() + key;
            jedis.set(realKey, value);
        } catch (Exception ex) {
            logger.error(ex.getMessage());
        } finally {
            returnResource(jedis);
        }
    }

    /**
     * Get key value pair
     *
     * @param keyPrefix
     * @param key
     * @return
     */
    public static String get(KeyPrefix keyPrefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = keyPrefix.getPrefix() + key;
            return jedis.get(realKey);
        } catch (Exception ex) {
            return null;
        } finally {
            returnResource(jedis);
        }
    }

    /**
     * Subtract the value of the given key by 1
     *
     * @param keyPrefix
     * @param key
     */
    public static void decr(KeyPrefix keyPrefix, String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = keyPrefix.getPrefix() + key;
            jedis.decr(realKey);
        } catch (Exception ex) {

        } finally {
            returnResource(jedis);
        }
    }
}

 

I used the test tool to test it myself. Take the example of cutting inventory of seckill goods.

Test example Controller:

package com.lds.springbootdemo.controller.test;

import com.lds.springbootdemo.domain.vo.ResponseBO;
import com.lds.springbootdemo.redis.JedisUtil;
import com.lds.springbootdemo.redis.keyPrefix.GoodsoldPrefix;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @program: core
 * @description: Sales controller
 * @author: lidongsheng
 * @createData: 2020-03-13 15:51
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 15:51
 * @updateContent: Sales controller
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */
@RestController
@RequestMapping("/goodsoldController")
@Api(tags = "High concurrency commodity sales test interface")
public class GoodsoldController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    //Lock expiration time (to prevent deadlock, generally speaking, the lock expiration time should be slightly longer than the execution time of the code in the specific business processing logic)
    private static Long LOCK_EXPIRE_TIME = 300L;

    private static Long stock = 1000L;
    //Timeout for obtaining lock waiting (generally less than the expiration time of lock, the reason is later)
    private static Long lockWaitTimeOut = 200L;


    @ApiOperation(value = "Sales less inventory")
    @GetMapping(value = "/sell")
    public ResponseBO sell() {
        try {
            String stockTemp = JedisUtil.get(new GoodsoldPrefix("sell"), "stock");
            //Judge inventory first and filter for the first time
            if (stockTemp == null || Integer.parseInt(stockTemp) <= 0) {
                return ResponseBO.responseFail("It's been snapped up!");
            }
            //value of lock = milliseconds of current system time + milliseconds of lock expiration time
            Long time = System.currentTimeMillis() + LOCK_EXPIRE_TIME;
            //Lock operation
            if (!JedisUtil.lock(new GoodsoldPrefix("sell"), "lock", String.valueOf(time), lockWaitTimeOut)) {
                return ResponseBO.responseFail("There are too many people in the rush, please try again later");
            }
            stockTemp = JedisUtil.get(new GoodsoldPrefix("sell"), "stock");
            //Judge the inventory again, because it is possible that the first time you judge the inventory, there is no lock, and it is likely to read dirty data under concurrent conditions
            if (stockTemp == null || Integer.parseInt(stockTemp) <= 0) {
                return ResponseBO.responseFail("It's been snapped up!");
            }
            //Reduce stock
            JedisUtil.decr(new GoodsoldPrefix("sell"), "stock");
            //Record the number of inventory reduction operations
            JedisUtil.incr(new GoodsoldPrefix("sell"), "test");
            JedisUtil.unLock(new GoodsoldPrefix("sell"), "lock", String.valueOf(time));
            return ResponseBO.Builder.init().setReasonMessage("Panic buying").build();
        } catch (Exception ex) {
            logger.error(ex.getMessage());
            return ResponseBO.responseFail("Current interface exception:" + ex.getMessage());
        }
    }

    @ApiOperation(value = "Sales plus inventory")
    @GetMapping(value = "/addStock")
    public ResponseBO addStock(Integer value) {
        try {
            String oldValue = JedisUtil.get(new GoodsoldPrefix("sell"), "stock");
            Integer newValue = oldValue == null ? value : Integer.valueOf(oldValue) + value;
            JedisUtil.set(new GoodsoldPrefix("sell"), "stock", newValue.toString());
            return ResponseBO.Builder.init().setReasonMessage("Add inventory succeeded").build();
        } catch (Exception ex) {
            return ResponseBO.responseFail("Exception in adding inventory:" + ex.getMessage());
        }
    }


}

 

 

redis is for convenience. windows can use the redisDesktopManager client. Download address: Redis Desktop Manager click download.

The test tool uses Apache Jmeter, which is still very good. Download address: Jmeter click download.

10000 concurrent, set to 100 inventory.

 

I set up a simulation of 10000 users at the same time to reduce the stock of seckill goods.

 

As you can see from the result, the number of successful inventory reduction is 100, and the inventory quantity in redis is 0. In the case of high concurrency, there is no reduction of inventory.

But after many tests, it is found that there are still problems in most cases in the library. Sometimes there will be a negative number, that is, oversold, always oversold several.

It shows that there are still some problems in the logic of obtaining lock and releasing lock.

Let's analyze the problems in the logic of obtaining and releasing locks.

package com.lds.springbootdemo.redis;

import com.alibaba.druid.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;


/**
 * @program: core
 * @description:
 * @author: lidongsheng
 * @createData: 2020-03-13 12:53
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 12:53
 * @updateContent:
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */
@Component
public class JedisUtil {

    private static JedisPool jedisPool;

    private static Logger logger = LoggerFactory.getLogger(JedisUtil.class);

    @Autowired
    public void setJedisPool(JedisPool jedisPool) {
        JedisUtil.jedisPool = jedisPool;
    }


    /**
     * Get lock
     *
     * @param keyPrefix       In order to facilitate the management of data in redis, a prefix class is added (under one module, the prefix can be unified, which is convenient for management and can also be removed)
     * @param key             All client key s are consistent
     * @param value           Expiration time stamp of the lock (use expiration time stamp to prevent one node from blocking and releasing the lock, and other nodes from obtaining the lock)
     * @param lockWaitTimeOut Wait timeout of acquiring lock in milliseconds (to prevent a large number of requests from continuously acquiring lock resources)
     * @return
     */
    public static boolean lock(KeyPrefix keyPrefix, String key, String value, Long lockWaitTimeOut) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //All key s are the same
            String realKey = keyPrefix.getPrefix() + key;
            //Get the timeout stamp of the lock
            Long deadTime = System.currentTimeMillis() + lockWaitTimeOut;
            //for loop is to try to acquire lock again and again in lockWaitTimeOut in case of failure to acquire lock
            for (; ; ) {
                //setnx is a string whose key is val when and only when the key does not exist. It returns 1. If the key exists, it does nothing and returns 0.
                //At this time, we can judge whether the lock is successful according to the return value of setNx. The return value of 1 indicates that no one owns the lock, and the return value of 0 indicates that someone else is holding the lock
                if (jedis.setnx(realKey, value) == 1) {
                    return true;
                }
                String currentValue = jedis.get(realKey);
                /* There will be A concurrency problem here, that is, when C gets the lock, but it expires, in the case of concurrency, many clients are waiting for the judgment to get the lock. Suppose A and B make the judgment statement at the same time
                 * Note here: A and B are distributed, not in a Jvm. currentValue assumes CT at this time
                 */
                if (!StringUtils.isEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) {
                    /* Since C's lock has expired, A and B have made A judgment statement at the same time, which has been executed here,
                     * It doesn't matter if you execute getSet at the same time. Because redis is single threaded, there will be no concurrency problem. AgetSet and BgetSet must have a priority in redis,
                     * So the final result of getting lock depends on the value,
                     * In the case of high concurrency, it is true that value will be equal, because value = current system time milliseconds + lock expiration time milliseconds
                     * In the case of high concurrency, it is really possible that the current system time milliseconds of clients A, B and C are the same at that time, so this leads to the possibility that the value is the same. There are the following situations:
                     * 1: A The value of B is the same as that of B
                     * 2: A Different value from B
                     * 3: A,B,C The value s of are all the same
                     *
                     *
                     * Case 1:
                     * Result:
                     * In normal operation, only one of A or B can get the lock, and the one who gets the lock can release the lock normally.
                     * Reason:
                     * Because when the value s of A and B are equal (AT=BT), A and B are used for getSet,
                     * Suppose that the getSet operation of A is executed in redis first. Now the value is AT, and the oldValue returned is the value expired by C: CT,
                     * B The getSet operation of is executed after A. AT this time, B overwrites AT to BT, and the oldValue returned is AT,
                     * A oldValue (CT) of = currentValue(CT). A will get the lock and release it normally (because even if B covers the value of a, BT=AT)
                     * B oldValue(AT) of! = currentValue(CT).  B didn't get the post. It's over.
                     *
                     * Case 2:
                     * Result:
                     * A Or B gets the lock, but the one who gets the lock (A or b) can't release the lock.
                     * Reason:
                     * Suppose that the getSet operation of A is executed in redis first. Now the value is AT, and the oldValue returned is the value expired by C: CT,
                     * B The getSet operation of is executed after A. AT this time, B overwrites AT to BT, and the oldValue returned is AT,
                     * So at this time, the oldValue (CT) of a = currentValue(CT). A will get the lock
                     * B oldValue(AT) of! =currentValue (CT), B did not get the lock,. But unfortunately, B has updated the value (AT) to BT before this judgment.
                     * So it will eventually lead to A situation: after A obtains the lock and executes the business, when releasing the lock, it will be found that the value is not AT, but BT, so the lock cannot be released. It is intuitive that we can see in redis that the key value pairs with locks will not be deleted.
                     *
                     * Case 3:
                     * Result:
                     * A Both B and B get the lock, but when they release the lock, they will release the lock that is not their own
                     * Reason:
                     * In the case of high concurrency, the value of A, B and C may enter the lock method at the same time to obtain the lock. The value passed in is the same, and then it is certain that A client will obtain the lock,
                     * Other clients will continue to try to acquire the lock within the lockWaitTimeOut time. Suppose C obtains the lock, A and B will try again and again to acquire the lock within the lockWaitTimeOut time,
                     * When the blocking time of C is too long and the lock of C expires, A and B are still trying to get the lockWaitTimeOut time of the lock. They just finish the judgment of whether the search expires and enter getSet
                     * At this point, there will be A problem. When both A and B have finished executing the getSet,
                     * currentValue = CT
                     * Suppose that the getSet operation of A is executed in redis first. Now the value is AT, and the oldValue returned is the value expired by C: CT,
                     * B The getSet operation of is executed after A. AT this time, B overwrites AT to BT, and the oldValue returned is AT,
                     * Because AT=BT=CT, the oldvalue of A or B is equal to currentvalue at this time.
                     * So A and B will finally get the lock. When releasing the lock, suppose A finishes first, and B is still executing the business. When A releases the lock, because AT=BT, it will release B's lock as well
                     * This kind of situation is very serious. It will lead to oversold in seckill. Under multiple tests, most of the inventory will eventually be negative.
                     */
                    String oldValue = jedis.getSet(realKey, value);
                    if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                        return true;
                    }
                }
                lockWaitTimeOut = deadTime - System.currentTimeMillis();
                //If the lock is not obtained successfully within the lockWaitTimeOut time, false will be returned. In the case of high concurrency, resources cannot be occupied all the time.
                if (lockWaitTimeOut <= 0L) {
                    return false;
                }
            }

        } catch (Exception ex) {
            logger.error(ex.getMessage());
            return false;
        } finally {
            returnResource(jedis);
        }
    }

    /**
     * Release lock
     *
     * @param keyPrefix
     * @param key
     * @param value
     */
    public static void unLock(KeyPrefix keyPrefix, String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = keyPrefix.getPrefix() + key;
            String currentValue = jedis.get(realKey);
            /* Compare the value of releasing lock with that of redis to prevent releasing locks of other clients
             * For example, when client A obtains the lock, the execution time of client A is relatively long, exceeding the expiration time of the lock, then B or C can obtain the lock
             * B After getting the lock, execute the business logic of B. during the execution of the business logic of B, A will not block any more at this time. After the execution, release the lock. If you do not compare value and release the lock directly, the lock of B will be released, so this is certainly incorrect.
             * So you have to compare value s
             */
            if (!StringUtils.isEmpty(currentValue)
                    && value.equals(currentValue)) {
                jedis.del(realKey);
            }
        } catch (Exception ex) {

        } finally {
            returnResource(jedis);
        }
    }

    /**
     * Return the resources of the connection pool to the connection pool
     *
     * @param jedis
     */
    public static void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

}

 

How can we avoid this kind of oversold?

In fact, the oversold problem is that at this expiration time, we should not deal with it by our logic. We should give it to redis. In redis, we can set the expiration time of a key, so we should put setnx and setex in an operation command to ensure the atomic execution of setnx and setex. The value of the lock must be unique. Because when releasing the lock, you still need to judge the value to avoid releasing the lock owned by other clients.

So you can use lua script to execute setnx and setex to ensure that the two commands are atomic.

The following is the transformation of the JedisUtil class:

That's right!!

package com.lds.springbootdemo.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


/**
 * @program: core
 * @description:
 * @author: lidongsheng
 * @createData: 2020-03-13 12:53
 * @updateAuthor: lidongsheng
 * @updateData: 2020-03-13 12:53
 * @updateContent:
 * @Version: 1.0.0
 * @email: lidongshenglife@163.com
 * @blog: www.b0c0.com
 * ************************************************
 * Copyright@Li Dongsheng 2020.Allrightsreserved
 * ************************************************
 */
@Component
public class NewJedisUtil {

    private static JedisPool jedisPool;

    private static Logger logger = LoggerFactory.getLogger(NewJedisUtil.class);

    //setNx and setEx lock atomic operation lua script
    private static final String SET_NX_EX_LOCK = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then return redis.call('pexpire',KEYS[1],tonumber(ARGV[2])) else return 0 end";
    //Delete lock lua script
    private static final String DEL_LOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    public void setJedisPool(JedisPool jedisPool) {
        NewJedisUtil.jedisPool = jedisPool;
    }


    /**
     * Get lock
     *
     * @param keyPrefix       In order to facilitate the management of data in redis, a prefix class is added (under one module, the prefix can be unified, which is convenient for management and can also be removed)
     * @param key             All client key s are consistent
     * @param value           Expiration time stamp of the lock (use expiration time stamp to prevent one node from blocking and releasing the lock, and other nodes from obtaining the lock)
     * @param lockWaitTimeOut Wait timeout of acquiring lock in milliseconds (to prevent a large number of requests from continuously acquiring lock resources)
     * @return
     */
    public static boolean lock(KeyPrefix keyPrefix, String key, String value, Long lockWaitTimeOut) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //All key s are the same
            String realKey = keyPrefix.getPrefix() + key;
            //Get the timeout stamp of the lock
            Long deadTime = System.currentTimeMillis() + lockWaitTimeOut;
            List<String> arvgs = new ArrayList<>(2);
            arvgs.add(value);
            arvgs.add(keyPrefix.expireMillisecond().toString());
            //The number returned by jedis.eval is Long. If it is converted to Integer, an error will be reported
            Long result = 0L;
            //for loop is to try to acquire lock again and again in lockWaitTimeOut in case of failure to acquire lock
            for (; ; ) {

                result = (Long) jedis.eval(SET_NX_EX_LOCK, Collections.singletonList(realKey), arvgs);
                if (1 == result) {
                    return true;
                }
                lockWaitTimeOut = deadTime - System.currentTimeMillis();
                //If the lock is not obtained successfully within the lockWaitTimeOut time, false will be returned. In the case of high concurrency, resources cannot be occupied all the time.
                if (lockWaitTimeOut <= 0L) {
                    return false;
                }
            }
        } catch (
                Exception ex) {
            logger.error(ex.getMessage());
            return false;
        } finally {
            returnResource(jedis);
        }

    }

    /**
     * Release lock
     *
     * @param keyPrefix
     * @param key
     * @param value
     */
    public static boolean unLock(KeyPrefix keyPrefix, String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = keyPrefix.getPrefix() + key;

            Long result = (Long) jedis.eval(DEL_LOCK, Collections.singletonList(realKey),
                    Collections.singletonList(value));
            if (1 == result) {
                return true;
            }
        } catch (Exception ex) {
            logger.error(ex.getMessage());
            return false;
        } finally {
            returnResource(jedis);
        }
        return false;
    }

    /**
     * Return the resources of the connection pool to the connection pool
     *
     * @param jedis
     */
    public static void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

   ...Omit some codes

}

 

Test method:

@ApiOperation(value = "Sales less inventory")
    @GetMapping(value = "/newSell")
    public ResponseBO newSell() {
        try {
            String stockTemp = NewJedisUtil.get(new GoodsoldPrefix("sell"), "stock");
            //Judge inventory first and filter for the first time
            if (stockTemp == null || Integer.parseInt(stockTemp) <= 0) {
                return ResponseBO.responseFail("It's been snapped up!");
            }
            String value = UUID.randomUUID().toString();
            //Lock operation
            if (!NewJedisUtil.lock(new GoodsoldPrefix(LOCK_EXPIRE_TIME, "sell"), "lock", value, lockWaitTimeOut)) {
                return ResponseBO.responseFail("There are too many people in the rush, please try again later");
            }
            stockTemp = NewJedisUtil.get(new GoodsoldPrefix("sell"), "stock");
            //Judge the inventory again, because it is possible that the first time you judge the inventory, there is no lock, and it is likely to read dirty data under concurrent conditions
            if (stockTemp == null || Integer.parseInt(stockTemp) <= 0) {
                return ResponseBO.responseFail("It's been snapped up!");
            }

            //Reduce stock
            NewJedisUtil.decr(new GoodsoldPrefix("sell"), "stock");
            //Record the number of inventory reduction operations
            NewJedisUtil.incr(new GoodsoldPrefix("sell"), "test");
            NewJedisUtil.unLock(new GoodsoldPrefix("sell"), "lock", value);
            return ResponseBO.Builder.init().setReasonMessage("Panic buying").build();
        } catch (Exception ex) {
            logger.error(ex.getMessage());
            return ResponseBO.responseFail("Current interface exception:" + ex.getMessage());
        }
    }

 

After several tests, there will be no more inventory reduction, and the lock key will be deleted normally, and there will be no last deletion of the lock.

 

 

Reference blog link:

https://www.jianshu.com/p/83224c0f3bb9

https://www.cnblogs.com/52fhy/p/9786720.html

Published 15 original articles, won praise 11, visited 20000+
Private letter follow

Posted by friendlylad on Mon, 16 Mar 2020 00:45:52 -0700