其實(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下觀察:

成功將五個(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

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

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

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

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

接下來的工作就是宣布誰是手氣最佳,和把hongBaoConsumedList的數(shù)據(jù)塞到DataBase里面。
參考:
紅包生成算法:http://www.jb51.net/article/98620.htm
白賀翔老師之前的Redis+Lua視頻