Using redis + lua to solve the problem of high concurrency

  • 2020-05-15 02:31:37
  • OfStack

Demand analysis of snatching red envelopes

The scene of grabbing a red envelope is a bit like seckilling, but simpler than seckilling.

Because seckill is usually related to inventory. Snatching red envelopes allows some of the money not to be snatched, because the sender will not lose, and the money will be returned to the sender.

In addition, the shopping like xiaomi is simpler than that of taobao. It is also because xiaomi is a company. If there is a small amount that is not snatched, it will be snatched again next time. And if there are so many products like taobao, if every one of them is at risk of repairing the data, it will be very troublesome if it goes wrong.

The red envelope grabbing scheme based on redis

Here is a scheme based on Redis.

The original red envelope is called a big red envelope and the split red envelope is called a small red envelope.

1. The small red envelope is generated in advance and inserted into the database. The corresponding user ID is null. Generation algorithm: see another 1 article / / www ofstack. com article / 98620. htm

2. For each large red envelope, there are two redis queues, one is the unconsumed red envelope queue, and the other is the consumed red envelope queue. At the beginning, put all the unsnatched small red envelopes into the queue of unspent red envelopes.

In the queue of unconsumed red packets are json strings, such as {userId:'789', money:'300'}.

3. Use one map in redis to filter users who have already grabbed the red envelope.

4. When grabbing a red envelope, judge whether the user has snatched the red envelope first. If not, a small red envelope will be taken from the queue of never consuming the red envelope, then push will be transferred to another consumed queue, and finally the user ID will be put into the heavy map.

5. Use one single thread batch to take out the red packets in the consumption queue, and then batch ID of update red packets into the database.

The above procedure is very clear, but in step 4, if the user clicks twice quickly or opens two browsers to grab the red envelope, will it be possible for the user to grab two red envelopes?

To solve this problem, an lua script is used to make the whole process of step 4 atomically executed.

Here is the Lua script executed on redis:


--  Function: attempts to get a red envelope, returns if successful json String, return empty if unsuccessful  
--  Parameter: queue name of red envelope,   The consumed queue name, deduplication Map Name, the user ID 
--  The return value: nil  or  json String containing the user ID : userId , a red envelope ID : id , amount of red envelope: money 
 
--  If the user has snatched the red envelope, return it nil 
if rediscall('hexists', KEYS[3], KEYS[4]) ~= 0 then 
 return nil 
else 
 --  First the 1 A small red envelope  
 local hongBao = rediscall('rpop', KEYS[1]); 
 if hongBao then 
  local x = cjsondecode(hongBao); 
  --  Add a user ID information  
  x['userId'] = KEYS[4]; 
  local re = cjsonencode(x); 
  --  The user ID Put it in the heavy one set In the  
  rediscall('hset', KEYS[3], KEYS[4], KEYS[4]); 
  --  Put the red envelope in the consumption queue  
  rediscall('lpush', KEYS[2], re); 
  return re; 
 end 
end 
return nil 

Here's the test code:


public class TestEval { 
  static String host = "localhost"; 
  static int honBaoCount = 1_0_0000; 
   
  static int threadCount = 20; 
   
  static String hongBaoList = "hongBaoList"; 
  static String hongBaoConsumedList = "hongBaoConsumedList"; 
  static String hongBaoConsumedMap = "hongBaoConsumedMap"; 
   
  static Random random = new Random(); 
   
// --  Function: attempts to get a red envelope, returns if successful json String, return empty if unsuccessful  
// --  Parameter: queue name of red envelope,   The consumed queue name, deduplication Map Name, the user ID 
// --  The return value: nil  or  json String containing the user ID : userId , a red envelope ID : id , amount of red envelope: money 
  static String tryGetHongBaoScript =  
//     "local bConsumed = rediscall('hexists', KEYS[3], KEYS[4]);\n" 
//     + "print('bConsumed:' ,bConsumed);\n" 
      "if rediscall('hexists', KEYS[3], KEYS[4]) ~= 0 then\n" 
      + "return nil\n" 
      + "else\n" 
      + "local hongBao = rediscall('rpop', KEYS[1]);\n" 
//     + "print('hongBao:', hongBao);\n" 
      + "if hongBao then\n" 
      + "local x = cjsondecode(hongBao);\n" 
      + "x['userId'] = KEYS[4];\n" 
      + "local re = cjsonencode(x);\n" 
      + "rediscall('hset', KEYS[3], KEYS[4], KEYS[4]);\n" 
      + "rediscall('lpush', KEYS[2], re);\n" 
      + "return re;\n" 
      + "end\n" 
      + "end\n" 
      + "return nil"; 
  static StopWatch watch = new StopWatch(); 
   
  public static void main(String[] args) throws InterruptedException { 
//   testEval(); 
    generateTestData(); 
    testTryGetHongBao(); 
  } 
   
  static public void generateTestData() throws InterruptedException { 
    Jedis jedis = new Jedis(host); 
    jedisflushAll(); 
    final CountDownLatch latch = new CountDownLatch(threadCount); 
    for(int i = 0; i < threadCount; ++i) { 
      final int temp = i; 
      Thread thread = new Thread() { 
        public void run() { 
          Jedis jedis = new Jedis(host); 
          int per = honBaoCount/threadCount; 
          JSONObject object = new JSONObject(); 
          for(int j = temp * per; j < (temp+1) * per; j++) { 
            objectput("id", j); 
            objectput("money", j); 
            jedislpush(hongBaoList, objecttoJSONString()); 
          } 
          latchcountDown(); 
        } 
      }; 
      threadstart(); 
    } 
    latchawait(); 
  } 
   
  static public void testTryGetHongBao() throws InterruptedException { 
    final CountDownLatch latch = new CountDownLatch(threadCount); 
    Systemerrprintln("start:" + SystemcurrentTimeMillis()/1000); 
    watchstart(); 
    for(int i = 0; i < threadCount; ++i) { 
      final int temp = i; 
      Thread thread = new Thread() { 
        public void run() { 
          Jedis jedis = new Jedis(host); 
          String sha = jedisscriptLoad(tryGetHongBaoScript); 
          int j = honBaoCount/threadCount * temp; 
          while(true) { 
            Object object = jediseval(tryGetHongBaoScript, 4, hongBaoList, hongBaoConsumedList, hongBaoConsumedMap, "" + j); 
            j++; 
            if (object != null) { 
//             Systemoutprintln("get hongBao:" + object); 
            }else { 
              // I've done that  
              if(jedisllen(hongBaoList) == 0) 
                break; 
            } 
          } 
          latchcountDown(); 
        } 
      }; 
      threadstart(); 
    } 
     
    latchawait(); 
    watchstop(); 
     
    Systemerrprintln("time:" + watchgetTotalTimeSeconds()); 
    Systemerrprintln("speed:" + honBaoCount/watchgetTotalTimeSeconds()); 
    Systemerrprintln("end:" + SystemcurrentTimeMillis()/1000); 
  } 
} 

The test results showed that 20 threads can grab 25,000 pieces per second, which is enough to handle most hongbao scenarios.

If you really can't handle it, split it up into several redis clusters, or grab the red envelopes in batches instead.

Conclusion:

The redis scheme, which loses one second of data in extreme cases (i.e., when redis fails), is scalable enough to handle high concurrency.


Related articles: