詳解利用redis + lua解決搶紅包高並發的問題_Redis

來源:互聯網
上載者:User

搶紅包的需求分析

搶紅包的情境有點像秒殺,但是要比秒殺簡單點。

因為秒殺通常要和庫存相關。而搶紅包則可以允許有些紅包沒有被搶到,因為發紅包的人不會有損失,沒搶完的錢再退回給發紅包的人即可。

另外像小米這樣的搶購也要比淘寶的要簡單,也是因為像小米這樣是一個公司的,如果有少量沒有搶到,則下次再搶,人工修複下資料是很簡單的事。而像淘寶這麼多商品,要是每一個都存在著修複資料的風險,那如果出故障了則很麻煩。

基於redis的搶紅包方案

下面介紹一種基於Redis的搶紅包方案。

把原始的紅包稱為大紅包,拆分後的紅包稱為小紅包。

1.小紅包預先產生,插到資料庫裡,紅包對應的使用者ID是null。產生演算法見另一篇文章:http://www.jb51.net/article/98620.htm

2.每個大紅包對應兩個redis隊列,一個是未消費紅包隊列,另一個是已消費紅包隊列。開始時,把未搶的小紅包全放到未消費紅包隊列裡。

未消費紅包隊列裡是json字串,如{userId:'789', money:'300'}。

3.在redis中用一個map來過濾已搶到紅包的使用者。

4.搶紅包時,先判斷使用者是否搶過紅包,如果沒有,則從未消費紅包隊列中取出一個小紅包,再push到另一個已消費隊列中,最後把使用者ID放入去重的map中。

5.用一個單線程批量把已消費隊列裡的紅包取出來,再批量update紅包的使用者ID到資料庫裡。

上面的流程是很清楚的,但是在第4步時,如果是使用者快速點了兩次,或者開了兩個瀏覽器來搶紅包,會不會有可能使用者搶到了兩個紅包?

為瞭解決這個問題,採用了lua指令碼方式,讓第4步整個過程是原子性地執行。

下面是在redis上執行的Lua指令碼:

-- 函數:嘗試獲得紅包,如果成功,則返回json字串,如果不成功,則返回空 -- 參數:紅包隊列名, 已消費的隊列名,去重的Map名,使用者ID -- 傳回值:nil 或者 json字串,包含使用者ID:userId,紅包ID:id,紅包金額:money  -- 如果使用者已搶過紅包,則返回nil if rediscall('hexists', KEYS[3], KEYS[4]) ~= 0 then  return nil else  -- 先取出一個小紅包  local hongBao = rediscall('rpop', KEYS[1]);  if hongBao then   local x = cjsondecode(hongBao);   -- 加入使用者ID資訊   x['userId'] = KEYS[4];   local re = cjsonencode(x);   -- 把使用者ID放到去重的set裡   rediscall('hset', KEYS[3], KEYS[4], KEYS[4]);   -- 把紅包放到已消費隊列裡   rediscall('lpush', KEYS[2], re);   return re;  end end return nil 

下面是測試代碼:

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();    // -- 函數:嘗試獲得紅包,如果成功,則返回json字串,如果不成功,則返回空 // -- 參數:紅包隊列名, 已消費的隊列名,去重的Map名,使用者ID // -- 傳回值:nil 或者 json字串,包含使用者ID:userId,紅包ID:id,紅包金額: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 {               //已經取完了               if(jedisllen(hongBaoList) == 0)                 break;             }           }           latchcountDown();         }       };       threadstart();     }          latchawait();     watchstop();          Systemerrprintln("time:" + watchgetTotalTimeSeconds());     Systemerrprintln("speed:" + honBaoCount/watchgetTotalTimeSeconds());     Systemerrprintln("end:" + SystemcurrentTimeMillis()/1000);   } } 

測試結果20個線程,每秒可以搶2.5萬個,足以應付絕大部分的搶紅包情境。

如果是真的應付不了,拆分到幾個redis叢集裡,或者改為批量搶紅包,也足夠應付。

總結:

redis的搶紅包方案,雖然在極端情況下(即redis掛掉)會丟失一秒的資料,但是卻是一個擴充性很強,足以應付高並發的搶紅包方案。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.