利用Redis和Lua的原子性實(shí)現(xiàn)搶紅包功能

其實(shí)8月的時(shí)候就準(zhǔn)備搞這個(gè)搶紅包業(yè)務(wù),代替消費(fèi)后每天固定紅包的業(yè)務(wù)。但后來公司發(fā)展需要,希望固定返利維持現(xiàn)狀,紅包功能戛然而止。寫到簡書做個(gè)記錄。

設(shè)計(jì)思路:

準(zhǔn)備3個(gè)隊(duì)列

第一:生成紅包隊(duì)列hongBaoList,比如100元,分成10個(gè),每個(gè)紅包在10元上下波動(dòng),波動(dòng)范圍在[min, max],并呈現(xiàn)一個(gè)正態(tài)分布。這個(gè)point不是我在這里分享的關(guān)鍵,大家可以click進(jìn)這條link:http://www.jb51.net/article/98620.htm,看下生成算法見的一篇文章。

第二:已消費(fèi)的紅包隊(duì)列hongBaoConsumedList,就是我每消費(fèi)一個(gè)紅包,hongBaoList減少一個(gè),hongBaoConsumedList多增加一個(gè),知道hongBaoList消費(fèi)完。

第三:去重的隊(duì)列hongBaoConsumedMap,記錄已經(jīng)搶了紅包的用戶ID,就是防止用戶搶多個(gè)紅包。當(dāng)然放在hongBaoConsumedList也行,但比較麻煩,索性新起一個(gè)map,記錄已經(jīng)搶了紅包的用戶ID,到時(shí)用redis的hexists直接判斷用戶ID有沒有在去重的Map里面。

為什么用redis+lua:

第一:redis緩存的讀寫快,lua的輕量級(jí)開發(fā)。

第二:redis具有原子性,也就是單線程,所以操作紅包是安全的,但對(duì)于一個(gè)列表是安全的,對(duì)于多個(gè)列表,像hongBaoList,hongBaoConsumedList,hongBaoConsumedMap這三個(gè)列表進(jìn)行邏輯操作,就需要lua腳本,lua也有原子性,而且能保證hongBaoList,hongBaoConsumedList,hongBaoConsumedMap這三個(gè)列表在同意線程下統(tǒng)一操作。

所以選擇redis+lua來解決搶紅包高并發(fā)的問題。

實(shí)現(xiàn)步驟:

1:按照http://www.jb51.net/article/98620.htm ,寫一個(gè)生成紅包的類HongBaoCreateUtil,:

import java.util.Random;

public class HongBaoCreateUtil {  
static Random random = new Random();  
static {  
    random.setSeed(System.currentTimeMillis());  
}  
  
public static void main(String[] args) {  
    long max = 3;  
    long min = 1;  

    long[] result = HongBaoCreateUtil.generate(10, 5, max, min);  
    long total = 0;  
    for (int i = 0; i < result.length; i++) {  
         System.out.println("result[" + i + "]:" + result[i]);  
         System.out.println(result[i]);  
        total += result[i];  
    }  
    //檢查生成的紅包的總額是否正確  
    System.out.println("total:" + total);  

    //統(tǒng)計(jì)每個(gè)錢數(shù)的紅包數(shù)量,檢查是否接近正態(tài)分布  
    int count[] = new int[(int) max + 1];  
    for (int i = 0; i < result.length; i++) {  
        count[(int) result[i]] += 1;  
    }  

    for (int i = 0; i < count.length; i++) {  
        System.out.println("" + i + "  " + count[i]);  
    }  
}  
  
/** 
 * 生產(chǎn)min和max之間的隨機(jī)數(shù),但是概率不是平均的,從min到max方向概率逐漸加大。 
 * 先平方,然后產(chǎn)生一個(gè)平方值范圍內(nèi)的隨機(jī)數(shù),再開方,這樣就產(chǎn)生了一種“膨脹”再“收縮”的效果。 
 *  
 * @param min 
 * @param max 
 * @return 
 */  
static long xRandom(long min, long max) {  
    return sqrt(nextLong(sqr(max - min)));  
}  

/** 
 *  
 * @param total 
 *            紅包總額 
 * @param count 
 *            紅包個(gè)數(shù) 
 * @param max 
 *            每個(gè)小紅包的最大額 
 * @param min 
 *            每個(gè)小紅包的最小額 
 * @return 存放生成的每個(gè)小紅包的值的數(shù)組 
 */  
public static long[] generate(long total, int count, long max, long min) {  
    long[] result = new long[count];  

    long average = total / count;  

    long a = average - min;  
    long b = max - min;  

    //  
    //這樣的隨機(jī)數(shù)的概率實(shí)際改變了,產(chǎn)生大數(shù)的可能性要比產(chǎn)生小數(shù)的概率要小。  
    //這樣就實(shí)現(xiàn)了大部分紅包的值在平均數(shù)附近。大紅包和小紅包比較少。  
    long range1 = sqr(average - min);  
    long range2 = sqr(max - average);  

    for (int i = 0; i < result.length; i++) {  
        //因?yàn)樾〖t包的數(shù)量通常是要比大紅包的數(shù)量要多的,因?yàn)檫@里的概率要調(diào)換過來。  
        //當(dāng)隨機(jī)數(shù)>平均值,則產(chǎn)生小紅包  
        //當(dāng)隨機(jī)數(shù)<平均值,則產(chǎn)生大紅包  
        if (nextLong(min, max) > average) {  
            // 在平均線上減錢  
            //long temp = min + sqrt(nextLong(range1));  
            long temp = min + xRandom(min, average);  
            result[i] = temp;  
            total -= temp;  
        } else {  
            // 在平均線上加錢  
            //long temp = max - sqrt(nextLong(range2));  
            long temp = max - xRandom(average, max);  
            result[i] = temp;  
            total -= temp;  
        }  
    }  
    // 如果還有余錢,則嘗試加到小紅包里,如果加不進(jìn)去,則嘗試下一個(gè)。  
    while (total > 0) {  
        for (int i = 0; i < result.length; i++) {  
            if (total > 0 && result[i] < max) {  
                result[i]++;  
                total--;  
            }  
        }  
    }  
    // 如果錢是負(fù)數(shù)了,還得從已生成的小紅包中抽取回來  
    while (total < 0) {  
        for (int i = 0; i < result.length; i++) {  
            if (total < 0 && result[i] > min) {  
                result[i]--;  
                total++;  
            }  
        }  
    }  
    return result;  
}  

static long sqrt(long n) {  
    // 改進(jìn)為查表?  
    return (long) Math.sqrt(n);  
}  

static long sqr(long n) {  
    // 查表快,還是直接算快?  
    return n * n;  
}  
  
static long nextLong(long n) {  
    return random.nextInt((int) n);  
}  

static long nextLong(long min, long max) {  
    return random.nextInt((int) (max - min + 1)) + min;  
}  
}  

2.將生成的紅包放進(jìn)redis的hongBaoList

static public void generateTestData() throws InterruptedException {  
    Jedis jedis = new Jedis(host, port); 
    jedis.flushAll();  
    
    int total = 10;//10塊紅包
    int count  = 5;//分成5份
    long max = 3;  //最大值3塊
    long min = 1;  //最小值1塊

    long[] result = HongBaoCreateUtil.generate(total, count, max, min);  
    JSONObject object = new JSONObject();  
    for (long l : result) {
        object.put("id", l);  
        object.put("money", l);  
        jedis.lpush(hongBaoList, object.toJSONString());  
    }
    jedis.close();
}  

main函數(shù)run一下,在RedisDesktopManager下觀察:

hongBaoList.png

成功將五個(gè)紅包塞進(jìn)redis

3.寫線程模擬搶紅包:

    static String tryGetHongBaoScript =   
        "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n"  
        + "return nil\n"  
        + "else\n"  
        + "local hongBao = redis.call('rpop', KEYS[1]);\n"  
        + "if hongBao then\n"  
        + "local x = cjson.decode(hongBao);\n"  
        + "x['userId'] = KEYS[4];\n"  
        + "local re = cjson.encode(x);\n"  
        + "redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n"  
        + "redis.call('lpush', KEYS[2], re);\n"  
        + "return re;\n"  
        + "end\n"  
        + "end\n"  
        + "return nil";  
  static public void testTryGetHongBao() throws InterruptedException {  
    final CountDownLatch latch = new CountDownLatch(threadCount);  
    
    long startTime = System.currentTimeMillis();
    System.err.println("start:" + startTime);  
    
    for(int i = 0; i < threadCount; ++i) {  
        final int temp = i;  
        Thread thread = new Thread() {  
            public void run() {  
                Jedis jedis = new Jedis(host, port);  
                String sha = jedis.scriptLoad(tryGetHongBaoScript);  
                int j = honBaoCount/threadCount * temp;  
                while(true) {  
                    //搶紅包方法
                    Object object = jedis.eval(tryGetHongBaoScript, 4, 
                            hongBaoList/*預(yù)生成的紅包隊(duì)列*/, 
                            hongBaoConsumedList, /*已經(jīng)消費(fèi)的紅包隊(duì)列*/
                            hongBaoConsumedMap, /*去重的map*/
                            "" + j  /*用戶id*/
                            );  
                    j++;  
                    if (object != null) {  
                        //do something...
  //                          System.out.println("get hongBao:" + object);  
                    }else {  
                        //已經(jīng)取完了  
                        if(jedis.llen(hongBaoList) == 0)  
                            break;  
                    }  
                }  
                latch.countDown();  
            }  
        };  
        thread.start();  
    }  
      
    latch.await();  
    long costTime =  System.currentTimeMillis() - startTime;
    
    System.err.println("costTime:" + costTime);  
}  

其中tryGetHongBaoScript 是lua腳本:
/**
* -- 函數(shù):嘗試獲得紅包,如果成功,則返回json字符串,如果不成功,則返回空
* -- 參數(shù):紅包隊(duì)列名, 已消費(fèi)的隊(duì)列名,去重的Map名,用戶ID
* -- 返回值:nil 或者 json字符串,包含用戶ID:userId,紅包ID:id,紅包金額:money
*
* -- 如果用戶已搶過紅包,則返回nil
* if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then
* return nil
* else
* -- 先取出一個(gè)小紅包
* local hongBao = redis.call('rpop', KEYS[1]);
* if hongBao then
* local x = cjson.decode(hongBao);
* -- 加入用戶ID信息
* x['userId'] = KEYS[4];
* local re = cjson.encode(x);
* -- 把用戶ID放到去重的set里
* redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);
* -- 把紅包放到已消費(fèi)隊(duì)列里
* redis.call('lpush', KEYS[2], re);
* return re;
* end
* end
* return nil
*/

打個(gè)斷點(diǎn),main函數(shù)debug一下:

程序運(yùn)行到一半時(shí):

多了已消費(fèi)了的紅包:hongBaoConsumedList;去重表:hongBaoConsumedMap


3list

之前紅包列表少了兩個(gè)包:


l1.png

已消費(fèi)的紅包列表hongBaoConsumedList多了兩條數(shù)據(jù):


l3.png

去重表hongBaoConsumedMap多了兩條用戶的id:


l2.png

程序運(yùn)行完成后時(shí),只剩下hongBaoConsumedList和hongBaoConsumedMap,hongBaoConsumedList如圖:


接下來的工作就是宣布誰是手氣最佳,和把hongBaoConsumedList的數(shù)據(jù)塞到DataBase里面。

參考:
紅包生成算法:http://www.jb51.net/article/98620.htm
白賀翔老師之前的Redis+Lua視頻

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容