方案:
- 小红包预先生成,插到数据库里,红包对应的用户ID是null。
- 每个大红包对应两个redis队列,一个是未消费红包队列,另一个是已消费红包队列。开始时,把未抢的小红包全放到未消费红包队列里。 未消费红包队列里是json字符串,如{userId:’789’, money:’300’}。
- 在redis中用一个map来过滤已抢到红包的用户。
- 抢红包时,先判断用户是否抢过红包,如果没有,则从未消费红包队列中取出一个小红包,再push到另一个已消费队列中,最后把用户ID放入去重的map中。
- 用一个单线程批量把已消费队列里的红包取出来,再批量update红包的用户ID到数据库里。
实现:
package com.test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import org.springframework.util.StopWatch;
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
public class TestEval {
static String host = "localhost";
static int redPacketCount = 1_0_0000; //红包数量
static int threadCount = 1000; //开辟20个线程
static String redPacketList = "redPacketList"; //未消费的红包
static String redPacketConsumedList = "redPacketConsumedList"; //已消费的红包
static String redPacketConsumedMap = "redPacketConsumedMap"; //去重的map
static Random random = new Random();
// -- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
// -- 参数:红包队列名, 已消费的队列名,去重的Map名,用户ID
// -- 返回值: json字符串,包含用户ID:userId,红包ID:id,红包金额:money
static String tryGetRedPacketScript =
//如果用户已抢过红包,则返回nil
"if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n"
+ "return nil\n"
+ "else\n"
// 先取出一个小红包
+ "local redPacket = redis.call('rpop', KEYS[1]);\n"
+ "if redPacket then\n"
+ "local x = cjson.decode(redPacket);\n"
// 加入用户ID信息
+ "x['userId'] = KEYS[4];\n"
+ "local re = cjson.encode(x);\n"
//把用户ID放到去重的set里
+ "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 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);
jedis.flushAll();
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 = redPacketCount/threadCount;
JSONObject object = new JSONObject();
for(int j = temp ; j < per; j++) {
object.put("id", j);
object.put("money", j);
jedis.lpush(redPacketList, object.toJSONString());
}
latch.countDown();
}
};
thread.start();
}
latch.await();
}
static public void testTryGetHongBao() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(threadCount);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long startTime = System.currentTimeMillis();
Date date = new Date(startTime);
System.err.println("start:" + formatter.format(date));
watch.start();
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 = jedis.scriptLoad(tryGetRedPacketScript);
int j = redPacketCount/threadCount * temp;
while(true) {
Object object = jedis.eval(tryGetRedPacketScript, 4, redPacketList, redPacketConsumedList, redPacketConsumedMap, "" + j);
j++;
if (object != null) {
// System.out.println("get hongBao:" + object);
}else {
//已经取完了
if(jedis.llen(redPacketList) == 0)
break;
}
}
latch.countDown();
}
};
thread.start();
}
latch.await();
watch.stop();
System.err.println("time:" + watch.getTotalTimeSeconds());
System.err.println("speed:" + redPacketCount/watch.getTotalTimeSeconds());
long endTime = System.currentTimeMillis();
date = new Date(endTime);
System.err.println("end:" + formatter.format(date));
}
}
测试:
当线程开到1000时,每秒可以抢接近2.5万的红包。