腾讯赢了! 470万!用Redis+lua教你实现抢红包

近日,北京知识产权法院在官方微信发文称,掌上远景公司开发并运营了一款名为“微信自动抢红包”软件,卓易讯畅公司运营的豌豆荚应用开发平台提供下载该软件。腾讯科技公司、腾讯计算机公司以不正当竞争为由,将掌上远景公司和卓易讯畅公司诉至北京知识产权法院。

最终,法院认定掌上远景公司构成不正当竞争,判决掌上远景公司赔偿二原告经济损失450万元及合理支出约25.4万元。双方当事人均未提出上诉,目前该案已生效。

那我们的抢红包是怎么实现的呢?我来一步一步的教你,用个实例

需求:用户分享红包到微信群中。

每一个用户只能领取一个红包。

比如饿了么的红包分享:

在设计之前,先了解一下redis的list的数据结构:

1、lpush+lpop=Stack(栈)

2、lpush+rpop=Queue(队列)

3、lpsh+ltrim=Capped Collection(有限集合)

4、lpush+brpop=Message Queue(消息队列)

我们基于lpush+rpop进行红包的设计。如何设计?

1、当用户点击分享按钮,首先会给该订单生成若干个红包,将该红包push到redis中。

key的设计: hb:pool:{orderId}

value : n个红包的List队列,用来表示n个红包的池子

2、记录哪些用户已抢过红包,防止重复抢:从红包池中rpop(弹出)一个红包,就需要记录下来该用户领取了红包,这里用了hash结构。

key:hb:rd:{orderId} 例如订单号为12345,key为hb:rd:12345, 有4个用户id分别为 11111,11112,11113,11114

hset hb:rd:12345 11111 1

hset hb:rd:12345 11112 1

hset hb:rd:12345 11113 1

hset hb:rd:12345 11114 1

如下图:

判断用户11111是否领过红包,可以根据 hexists hb:rd:12345 11111 进行判断,如果领过返回1,否则返回0;

3、记录用户抢了多少钱:这里采用list结构进行存储。

key:hb:detailList:{orderId}

value : 用户抢到的红包列表。

抢红包流程:

保证用户抢红包整个流程的原子操作就必然要引入lua脚本,redis+Lua可以保证多条命令组合的原子性。
lua脚本:

/** 
 * 脚本调用方式
 * Object object = jedisUtils.eval(LuaScript.getHbLua,//lua脚本 
4,//参数个数 RedisKeys.getHbPoolKey(orderId),//对应脚本里的KEYS[1] 
RedisKeys.getDetailListKey(orderId),//对应脚本里的KEYS[2] 
RedisKeys.getHbRdKey(orderId),//对应脚本里的KEYS[3] 
String.valueOf(userId));//对应脚本里的KEYS[4] 
* 
* 
*/
public static String getHbLua = 
//查询用户是否已抢过红包,如果用户已抢过红包,则直接返回
 "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n" + 
//如果抢过红包 返回“1” 
"return '1';\n" + "else\n" + 
//从红包池取出一个小红包
 "local hb = redis.call('rpop', KEYS[1]);\n" + 
//判断红包池的红包不为空
 "if hb then\n" + 
"local x = cjson.decode(hb);\n" + 
//将红包信息与用户ID信息绑定,表示该用户已抢到红包  
"x['userId'] = KEYS[4];\n" +
 "local re = cjson.encode(x);\n" +
 //记录用户已抢过 比如 hset hb:rd:{orderId}  {userId}  1 
"redis.call('hset', KEYS[3], KEYS[4], '1');\n" +
//将抢红包的结果详情存入hb:detailList:{orderId}
 "redis.call('lpush', KEYS[2], re);\n" + 
"return re;\n" + "else\n" +
 //如果红包已被抢完 返回“0” 
"return '0';" + 
"end\n" + 
"end\n" +
 "return nil";

生成红包:

/**
 * 生成红包 
* @param orderId 
*/ public void genRedpack(long orderId,int redPackCount){ 
Boolean exists = jedisUtils.exists(RedisKeys.getHbPoolKey(orderId));
 if (!exists){ 
//根据业务规则生成红包 
int totalAmount = 2000;//总的红包金额20元 也就是2000分 
int[] redpacks = doPartitionRedpack(totalAmount,redPackCount); 
String[] list = new String[redpacks.length]; 
//将生成的红包push到redis中 
for (int i = 0;i < redpacks.length; i++){ 
JSONObject object = new JSONObject();
 object.put("hbId", i); //红包ID
 object.put("amount", redpacks[i]); //红包金额,存的是分
 list[i] = object.toJSONString(); 
} 
jedisUtils.lpush(RedisKeys.getHbPoolKey(orderId),list); 
} 
} 

/** 
* 划分红包 * @param totalAmount 红包总额 单位:分
 * @param redPackCount 红包数量
 * @return
 */ 
private int[] doPartitionRedpack(int totalAmount,int redPackCount) { 
Random random = new Random(); 
int randomMax= totalAmount - redPackCount;//每个人至少分1分钱,2000 - 6 = 1994元 也就是要随机分的钱。
 //要把1994 随机分成6份,我们需要向1994 这个数字中插入5个点
 // 比如 6 100 500 500 1600 这5个数字把1994分成了6份:6分 94分 400分 0分 1000分 394分
 int[] posArray = new int[redPackCount-1]; 
for (int i = 0;i < posArray.length; i++){
 int pos = random.nextInt(randomMax);
 posArray[i] = pos; 
} 
Arrays.sort(posArray);//对数组进行排序 
//生成红包 
int[] redpacks = new int[redPackCount]; 
for (int i = 0;i <= posArray.length; i++){ 
if (i == 0){
 redpacks[i] = posArray[i] + 1;//第一份 
}else if(i == posArray.length){//如果循环到posArray.length,此时数组已越界1位,randomMax - 该值 + 1分钱=最后一份 
redpacks[i] = randomMax - posArray[i-1] + 1;
 }else {
 redpacks[i] = posArray[i] - posArray[i-1] + 1; 
} 
} 
return redpacks; 
}

上面首先给每个红包分1分钱,然后把剩下的钱通过插入(redPackCount-1)个板子,就将剩余的钱分为redPackCount份。每份钱加上1分钱,就是每个红包的大小。分完红包后,将红包push到redis中。
抢红包:

/** 
* 抢红包 
* @param userId 
* @param orderId 
*/ 
public String snatchRedpack(long userId,long orderId){ 
Object object = jedisUtils.eval(LuaScript.getHbLua,4, 
RedisKeys.getHbPoolKey(orderId),// 
RedisKeys.getDetailListKey(orderId),// 
RedisKeys.getHbRdKey(orderId),String.valueOf(userId)); 

return (String) object;
 }

抢红包只需要执行一下上面的lua脚本。

测试:
运行生成红包:这里生成5个红包,orderId为111111.

Test 
public void genRedpack(){ 
JedisUtils jedisUtils = new JedisUtils("127.0.0.1", 6379, "123456"); 
RedpackService redpackService = new RedpackService(jedisUtils);
 redpackService.genRedpack(111111,5); 
}

运行后,redis中红包池就生成好了


运行抢红包:这里模拟了100个人,也就是100个线程。这里用了CyclicBarrier,等到所有的线程都准备好,同时开抢。

@Test
 public void snatchRedpack() throws InterruptedException { 
JedisUtils jedisUtils = new JedisUtils("118.89.196.99", 6379, "123456"); 
RedpackService redpackService = new RedpackService(jedisUtils); 
IdWorker idWorker = new IdWorker(); 
int N = 100; 
CyclicBarrier barrier = new CyclicBarrier(N);
 
for (int i = 0;i<N;i++){ 
new Thread(()->{ 
long userId = idWorker.nextId(); 
try {
 System.out.println("用户"+userId+"准备抢红包"); 
barrier.await(); 
} catch (InterruptedException e) { 
e.printStackTrace(); 
} catch (BrokenBarrierException e) {
 e.printStackTrace();
} 
String result = redpackService.snatchRedpack(userId, 111111);
 if ("0".equals(result)){ 
System.out.println("用户" + userId + "未抢到红包,原因:红包已领完"); 
}else if ("1".equals(result)){ 
System.out.println("用户" + userId + "未抢到红包,原因:红包已领过");
 }else{ 
System.out.println("用户" + userId + "抢到红包:" + result); 
} 
},"thread"+i).start(); 
} 
Thread.sleep(Integer.MAX_VALUE);
 }

运行结果:

...... 
用户1078994397959344128准备抢红包 
用户1078994397959344129准备抢红包 
用户1078994397959344130准备抢红包 
用户1078994397959344131准备抢红包 
用户1078994397896429573抢到红包:{"userId":"1078994397896429573","hbId":2,"amount":126} 
用户1078994397888040960抢到红包:{"userId":"1078994397888040960","hbId":1,"amount":526} 
用户1078994397896429572抢到红包:{"userId":"1078994397896429572","hbId":5,"amount":666} 
用户1078994397950955528未抢到红包,原因:红包已领完 
用户1078994397925789696抢到红包:{"userId":"1078994397925789696","hbId":4,"amount":490} 
用户1078994397929984004未抢到红包,原因:红包已领完 
用户1078994397959344128抢到红包:{"userId":"1078994397959344128","hbId":3,"amount":192} 
用户1078994397955149824未抢到红包,原因:红包已领完 
用户1078994397900623875未抢到红包,原因:红包已领完 
用户1078994397929984006未抢到红包,原因:红包已领完 
用户1078994397892235264未抢到红包,原因:红包已领完 
.......

结果看出100个线程同时抢,只有5个人抢成功了。

查看redis里的数据:红包池子里的红包已被领完,对应红包用户信息也相应的生成。      

这样就好了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容