Redis distributed lock

Description

对于High Concurrency 的场景通常要考虑线程抢夺资源的问题。比如电商SecKill活动通常容易出现的超卖现象。比如下面这段代码当有多个线程共享了库存和订单部分数据,在高并发的场景下就会产生超卖的问题。

  public void orderProductMockDiffUser(String productId)
  {
    //1.query product stock, if stockNum == 0
    int stockNum = stock.get(productId);
    if(stockNum == 0) {
      throw new OrderException(100,"out of stock");
    }else {
      //2.create order with UID
      orders.put(KeyUtil.getUID(),productId);// may lead to oversold issue
      //3.reduce the stock
      stockNum =stockNum-1;
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      stock.put(productId,stockNum);
    }
  }

Solution

  • 最简单的解决办法是在函数上加synchronize关键字。但会带来几个问题:
    • 运行时间慢:synchronize本质上把线程推入到一个队列中,按照单线程的逻辑运行每一个线程
    • 如果系统被做成了cluster,那么还是会有问题。线程在不同node中运行得到的结果还是不一样
  public synchronize void orderProductMockDiffUser(String productId)
  {
    //1.query product stock, if stockNum == 0
    int stockNum = stock.get(productId);
    if(stockNum == 0) {
      throw new OrderException(100,"out of stock");
    }else {
      //2.create order with UID
      orders.put(KeyUtil.getUID(),productId);// may lead to oversold issue
      //3.reduce the stock
      stockNum =stockNum-1;
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      stock.put(productId,stockNum);
    }
  }

Apache ab 压测

 ab -n500 -c100 http://127.0.0.1:8080/sell/skill/order/123456
  • 使用Redis SETNX 和 GETSET两个命令就可以组合出一个简单的distributed lock,用以解决高并发引入的问题

    SETNX Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed.> SETNX is short for “SET if Not eXists”.

    GETSET Atomically sets key to value and returns the old value stored at key. Returns an error when key exists but does not hold a string value.

    1. 线程使用SETNX对资源进行加锁,如果加锁成功则当前线程获得了资源的使用权。(key:当前资源标识 value: 锁的过期时间(解决死锁问题))
    2. 如果加锁失败,有两种情况: 当前系统资源已经被其他线程占用; 上一个线程出现了异常但没有对锁进行释放). 此时应该先用GET方法取出Value值,如果过期则需要使用GETSET方法对Key进行重新设置,如果GETSET方法返回值与过期的VALUE值相同,证明当前线程成功获得了锁,如果不同则证明同一时间有其他线程进行了相同的操作并获得了锁。如果未过期返回false
        @Service("RedisLock")
        public class RedisLock {
    
        @Autowired
        StringRedisTemplate stringRedisTemplate;
    
        /**
        * lock
        * @param key  productId
        * @param value currentTime + timeout
        * @return
        */
        public boolean lock(String key, String value) {
            //current thread get the lock
            if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
            }
    
            //check if the lock is expired
            String currentValue = stringRedisTemplate.opsForValue().get(key);
            //lock is expired
            if (currentValue != null && currentValue.length() != 0 && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
            //current thread get the lock
            if (oldValue!=null && oldValue.length() != 0 && currentValue.equals(oldValue))
                return true;
            }
    
            return false;
        }
    
        /**
        * unlock
        * @param key productId
        */
        public void unlock(String key) {
            String value = stringRedisTemplate.opsForValue().get(key);
            if (value != null && value.length() != 0) {
            stringRedisTemplate.delete(key);
            }
        }
        }