redis高并发抽奖
2019独角兽企业重金招聘Python工程师标准>>>
redis高并发抽奖
代码有点缭乱,没时间整理,如果有误还请留言斧正。现在进入正题。
一、思路
1.奖品:
奖品分为奖品id(编号)、count(数量)、pointVal(价值)、remainCount (剩余数量)分为四个参数组成。
2.概率规则:
单个产品剩余数量/总产品剩余数量=单个产品概率*1000,这也就意味着每个奖品的概率都是随着数量变化的,这时候计算概率对数据在时间维度要求必须一致(数据快照)。
每个产品概率相加=1000,如果没有等于1*概率相乘的数说明数据快照有问题(我这里是乘1000所以等于1000)。
3.设计思路:
这个demo,我用的是取之于民用之于民的想法写的,保证每一个人都会中奖。是不是很开心???。首先我们得有甲乙双发,甲方就是抽奖系统的提供方,乙方就是我们这些抽奖的小百姓。甲方会先提供奖品出来,我这里是一,二,三,四奖项,有一个默认五等奖奖项(demo里面有一,二,三,四,五个奖项)。我会先计算甲方提供每个奖品的数量、价值然后计算所有奖品的总价值,然后用 总价值/每次抽奖的分数=总抽奖次数。总抽奖次数 - 每个奖品的 = 默认五等奖次数。这样我们就算出了所有奖品的数量(包括默认五等奖也就是安慰奖的次数)。没当抽奖总次数==0的时候就会自动轮询补充库存开始新的一轮抽奖。
中奖公式 :
总抽奖消耗积分上限值/每次抽奖消耗固定积分 = 总抽奖次数
总抽奖次数 - 已经抽奖数量 = 剩余抽奖次数
奖品类型 * 奖品数量 = 奖品总数量
奖品总数量 - 已中奖数量 = 剩余奖品数量
剩余抽奖次数/剩余奖品数量 = 剩余奖品中奖概率
详细算法可以看看这篇文章。
4.降级问题:
这个一开始我也考虑过使用降级处理,以防服务器并发过大GG。不过我考虑到跟我的设计思路不符合,会影响到概率的公平性,我就把那部分去掉了(如果你们项目需要可以自己进行降级,限流,不过这会损失一部分概率的公平性,直接导致的结果就真正的奖品往往都是在最后面出现)。
5.注意事项:
特别强调一点,每次轮询的时候判断数量一定要用 == 不用用 <= 周末吃过这个亏,结果看轮询数据快照的时候偶尔来个 -1,特蛋疼。花了很多时间排除问题。总结一点就是涉及的数量红线变更的必须用精确判断,不能范围判断。
6.技术难点:
一、概率的计算
二、库存变更
三、轮询策略
基本就上面三个点,我这里是灵活使用redis 做缓存共享(也可以使用db的悲观锁、乐观锁、版本号),使用了其中管道技术做时间维度上的奖品数据快照解决概率计算的正确性;事物技术解决库存变更;两者结合解决了轮询策略问题。做到了监控每个轮询前每一个奖品数量,精确到每个轮询每个用户所中的奖品和顺序。
二、代码干货
奖品类:
public class Award {
/**编号*/
public String id;
/**数量(该类奖品数量)*/
public int count;
/**价值(该类奖品价值积分)*/
public int pointVal;
/**剩余数量(该类奖品剩余数量)*/
public int remainCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getPointVal() {
return pointVal;
}
public void setPointVal(int pointVal) {
this.pointVal = pointVal;
}
public int getRemainCount() {
return remainCount;
}
public void setRemainCount(int remainCount) {
this.remainCount = remainCount;
}
/**
*
* @param id *编号*
* @param count 数量(该类奖品数量)*
* @param pointVal 价值(该类奖品价值积分)*
* @param remainCount 剩余数量(该类奖品剩余数量)
*/
public Award( String id, int count, int pointVal, int remainCount) {
this.id = id;
this.count = count;
this.pointVal = pointVal;
this.remainCount = remainCount;
}
@Override
public String toString() {
return "Award [id=" + id + ", count=" + count + ", pointVal="
+ pointVal + ", remainCount=" + remainCount + "]";
}
}
概率计算:
详细算法可以看看这篇文章。
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
/**
*
* @author XiongYC
* @date 2017年12月3日
*
*/
public class LotteryUtil {
public static String lottery(Jedis jedis) {
try {
/**
* 读取redis 缓存参数(使用watch 确保数据准确性)
*/
Set<String> set = jedis.smembers(TestDemo.PRIZE_LIST); //获取奖品列表(id)
List<String> list = new ArrayList<String>(set.size()+1); //奖品列表(有序)
//使用管道技术一次性获取所有奖品数量确保数据完整性和概率计算的正确性
Pipeline p = jedis.pipelined();
for (String id : set) {
list.add(id); //增加奖品列表
p.get(id);//获取换成奖品数量
}
list.add(TestDemo.RESIDUAL_QUANTITY);
p.get(TestDemo.RESIDUAL_QUANTITY);
List<Object> list1= p.syncAndReturnAll();//获取所有奖品的剩余数量
int totailCount = Integer.valueOf(String.valueOf(list1.get(list1.size()-1))); //获取剩余奖品总数
if (totailCount == 0) {
// 重置奖品
TestDemo.initData(jedis);
return "-1";
}
// 存储每个奖品新的概率区间
List<Float> proSection = new ArrayList<Float>();
proSection.add(0f); //起始区间
float totalPro = 0f; // 总的概率区间
for (int i = 0; i < list1.size()-1; i++) {
// awardCount += Float.valueOf(jedis.get(id)); //计算奖品现有总数量
//弹性计算每个奖品的概率(剩余奖品数量/剩余总奖品数量) 每个概率区间为奖品概率乘以1000(把三位小数换为整)
totalPro += (Float.valueOf(String.valueOf(list1.get(i))) / Float.valueOf(String.valueOf(list1.get(list1.size()-1)))) * 1000;
proSection.add(totalPro);
}
// 获取总的概率区间中的随机数
Random random = new Random();
float randomPro = (float) random.nextInt((int) totalPro);
for (int i = 0, size = proSection.size(); i < size; i++) {
if (randomPro >= proSection.get(i) && randomPro < proSection.get(i + 1)) {
return list.get(i);
}
}
} catch (Exception e) {
System.err.println("概率之外计算错误" + e.getMessage());
return null;
}
return null;
}
}
库存共享变更:
import java.util.List;
import java.util.UUID;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
*
* @author XiongYC
* @date 2017年12月3日
*
*/
public class MyRunnable1 implements Runnable {
private Jedis jedis = RedisPoolUtils.getJedis();
@Override
public void run() {
try {
// 查询剩余奖品总数
String key = getPrize();
System.err.println("线程" + Thread.currentThread().getName() + "中奖奖品id为:" + key);
} catch (Exception e) {
System.err.println("算法计算异常:异常原因 = " + e.getMessage());
} finally {
RedisPoolUtils.returnResourceObject(jedis);
}
}
private String getPrize() {
String key = LotteryUtil.lottery(jedis); //获取中奖奖品ID
jedis.watch(key,TestDemo.RESIDUAL_QUANTITY); //精确监控单个奖品剩余数
if("-1".equals(key) || "0".equals(jedis.get(key))){
jedis.unwatch();
key = getPrize();
}else{
// key = AvailablePrize(key);
Transaction tx = jedis.multi(); //开启redis事物
tx.incrBy(TestDemo.RESIDUAL_QUANTITY, -1); //减少总库存
tx.incrBy(key, -1); //减少中奖奖品总库存
List<Object> listObj = tx.exec(); //提交事务,如果此时watch key被改动了,则返回null
if (listObj != null) { //多个进程同时 key>0 key相等时
// String useId = UUID.randomUUID().toString();
jedis.sadd("failuse", UUID.randomUUID().toString() + key);
System.out.println("用户中奖成功!!!"); //中奖成功业务逻辑
} else {
key = getPrize(); //重新计算奖品
}
}
return key;
}
//是否是有效奖品
// private String AvailablePrize(String key) {
// int prizeNum = Integer.valueOf(jedis.get(key));
//
// //奖品无效重新计算验证
// if(prizeNum <= 0){
// AvailablePrize(LotteryUtil.lottery(jedis));
// }
// return key;
// }
}
初始化:
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Transaction;
/**
*
* @author XiongYC
* @date 2017年12月3日
*
*/
public class TestDemo {
/**
* 安慰奖
*/
public static final String CONSOLATION_PRIZE_BY_ID= "2017112400005";
/**
* 缓存总次数 安慰奖+奖品
*/
public static final String RESIDUAL_QUANTITY= "residualQuantity";
/**
* 奖品列表(ID)
*/
public static final String PRIZE_LIST= "prizeList";
/**
* 每日抽奖所需积分
*/
public static final String POINTS_REQUIRED_FOR_THE_LOTTERY = "pointsRequiredForTheLlottery";
/**
* 轮询成功次数
*/
public static final String POLLING_SUCCESS_NUM = "POLLING_SUCCESS_NUM";
/**
* 轮询失败次数
*/
public static final String POLLING_FAIL_NUM = "POLLING_FAIL_NUM";
/**
* 轮询奖品数量快照
*/
public static final String SNAPSHOT_LIST = "SNAPSHOT_LIST";
/**
*中奖用户记录
*/
public static final String FAILUSE = "FAILUSE";
/**
* 从redis连接池获取连接
*/
private static Jedis jedis = RedisPoolUtils.getJedis();
public static void main(String[] args) {
try {
// jedis.del(POLLING_SUCCESS_NUM,POLLING_FAIL_NUM,SNAPSHOT_LIST,FAILUSE);
// jedis.set(RESIDUAL_QUANTITY, "0");
jedis.flushAll();//清除数据库
// 初始化奖品参数
initData(jedis);
ExecutorService executor = Executors.newFixedThreadPool(50);
for (int i = 0; i <2400; i++) {
executor.execute(new MyRunnable1());
}
executor.shutdown();
} catch (Exception e) {
System.err.println("ERROR_MSG = " + e.getMessage());
}finally{
if(jedis!=null){
RedisPoolUtils.returnResourceObject(jedis);//释放redis资源
}
}
}
/**
* 初始化奖品参数
*/
public static void initData(Jedis jedis) {
Award award1 = new Award("2017112400001", 1, 2000, 1); //一等奖
Award award2 = new Award("2017112400002", 2, 1500, 2); //二等奖
Award award3 = new Award("2017112400003", 3, 1000, 3); //三等奖
Award award4 = new Award("2017112400004", 4, 500, 4); //四等奖
List<Award> list = new ArrayList<Award>();
list.add(award1);
list.add(award2);
list.add(award3);
list.add(award4);
int tailCount = 0; //总奖品数
int tailPoint = 0; //总奖品总积分值
/**
* 读取redis 缓存参数(使用watch 确保数据准确性)
*/
Set<String> set = jedis.smembers(TestDemo.PRIZE_LIST); //获取奖品列表(id)
//轮询快照
Pipeline p = jedis.pipelined();
for (String id : set) {
p.get(id);//获取换成奖品数量
}
p.get(RESIDUAL_QUANTITY);
List<Object> temp = p.syncAndReturnAll();//获取所有奖品的剩余数量
//开始初始化缓存奖品数据
for (int i = 0; i < list.size(); i++) {
jedis.sadd(PRIZE_LIST, list.get(i).id); //缓存奖品列表
jedis.set(list.get(i).id, String.valueOf(list.get(i).count));//缓存奖品数量
tailCount +=list.get(i).count; //计算总奖品数
tailPoint += list.get(i).count* list.get(i).pointVal; //计算奖品总积分值
}
jedis.set(POINTS_REQUIRED_FOR_THE_LOTTERY, "500"); //每次抽奖所需积分
int residualQuantity = tailPoint / Integer.valueOf(jedis.get(POINTS_REQUIRED_FOR_THE_LOTTERY)); //计算未中奖次数(安慰奖)
int missesNum = residualQuantity - tailCount; //安慰剂次数
jedis.watch(RESIDUAL_QUANTITY);
int count = 0;
//判断是否是初次轮询
if(jedis.exists(RESIDUAL_QUANTITY)){
count= Integer.valueOf(jedis.get(RESIDUAL_QUANTITY));
}
if(count == 0){
Transaction tx = jedis.multi();
tx.set(CONSOLATION_PRIZE_BY_ID, String.valueOf(missesNum)); //缓存安慰奖次数
tx.sadd(PRIZE_LIST, CONSOLATION_PRIZE_BY_ID); //缓存安慰奖到奖品列表
tx.set(RESIDUAL_QUANTITY, String.valueOf(residualQuantity)); // 缓存总次数 安慰奖+奖品
List<Object> obj = tx.exec();
if (obj != null) { // 多个进程同时 key>0 key相等时
jedis.incrBy(POLLING_SUCCESS_NUM,1);
jedis.sadd(SNAPSHOT_LIST, jedis.get(POLLING_SUCCESS_NUM)+temp.toString());
System.out.println("初始化成功=============================》!!!");
} else {
jedis.incrBy(POLLING_FAIL_NUM,1);
System.err.println("初始化失败=============================》!!!");
}
}else{
jedis.unwatch();
}
}
}
三、数据验证
案例:根据以上demo我们可以看出有五个奖品(四个真正的奖品,一个安慰奖),我们根据设置的四个奖品可以得出 总积分价值为10000积分,每次抽奖500积分,一共需要抽20次完成一轮轮询,其中真正的奖品为10个,其余的10为安慰奖个数。
论证:现在我们用50个线程跑2400个请求
预期效果:我们会有2400/20=120次轮询,每次轮询奖品剩余库存数据快照为 轮询次数+[0,0,0,0,0],每次轮询抽奖结果为一等奖1个,二等奖2个,三等奖3个。四等奖4个,五等奖10个(安慰奖),共20个奖品;
实际效果:
轮询次数120次。,--验证通过
每次轮询奖品剩余库存数据快照
我截取了最好一部分数据快照,可以看出数据条数是可以对上轮询次数的,快照数据也是没问题的。--验证通过
每次轮询抽奖结果 :我随机抽取了一个循环的奖品记录
初始化成功=============================》!!!
线程pool-1-thread-46中奖奖品id为:2017112400005
用户中奖成功!!!
用户中奖成功!!!
线程pool-1-thread-14中奖奖品id为:2017112400003
线程pool-1-thread-43中奖奖品id为:2017112400005
用户中奖成功!!!
线程pool-1-thread-30中奖奖品id为:2017112400005用户中奖成功!!!
线程pool-1-thread-36中奖奖品id为:2017112400001
用户中奖成功!!!
线程pool-1-thread-13中奖奖品id为:2017112400002
用户中奖成功!!!
线程pool-1-thread-49中奖奖品id为:2017112400005
用户中奖成功!!!
线程pool-1-thread-5中奖奖品id为:2017112400005
用户中奖成功!!!
线程pool-1-thread-25中奖奖品id为:2017112400005
用户中奖成功!!!
线程pool-1-thread-35中奖奖品id为:2017112400003
用户中奖成功!!!
线程pool-1-thread-18中奖奖品id为:2017112400003
用户中奖成功!!!
用户中奖成功!!!
线程pool-1-thread-25中奖奖品id为:2017112400004
用户中奖成功!!!
线程pool-1-thread-38中奖奖品id为:2017112400005
用户中奖成功!!!
线程pool-1-thread-15中奖奖品id为:2017112400004
用户中奖成功!!!
线程pool-1-thread-48中奖奖品id为:2017112400004
用户中奖成功!!!
线程pool-1-thread-7中奖奖品id为:2017112400005
用户中奖成功!!!
线程pool-1-thread-37中奖奖品id为:2017112400002
用户中奖成功!!!
线程pool-1-thread-12中奖奖品id为:2017112400004
线程pool-1-thread-31中奖奖品id为:2017112400005
用户中奖成功!!!
用户中奖成功!!!
线程pool-1-thread-1中奖奖品id为:2017112400005
后台成功中奖记录里面条数也对应上了2400请求(有的细心的小伙伴可能会直接去缓存里面按行数来去验证奖品数量,我这里用的是集合,小伙伴们可以自行缓存list存储验证)。--验证通过。
以上就是全部内容了,有点糙,还望见谅。
转载于:https://my.oschina.net/demons99/blog/2231163
redis高并发抽奖相关推荐
- redis高并发原理_Java中的42行代码中的URL缩短服务— Java(?!)Spring Boot + Redis...
redis高并发原理 显然,编写URL缩短服务是新的"世界,您好! "在物联网/微服务/时代的世界中. 一切始于在45行Scala中的URL缩短服务-整洁的Scala,以Spray ...
- java连接redis不稳定_java相关:jedispool连redis高并发卡死的问题
java相关:jedispool连redis高并发卡死的问题 发布于 2020-6-30| 复制链接 本篇文章主要介绍了jedispool连redis高并发卡死的问题,小妖觉得挺不错的,现在分享给大家 ...
- Redis高并发点赞
1.redis高并发点赞就是保护数据库进行的操作 原理:就是将点赞数和点赞用户先存入redis中(防止大量用户数据对数据库的操作),通过定时任务在将数据取出来. 操作: 1.先将前端的数据存入redi ...
- 解决redis高并发问题的几种思路
解决redis高并发问题的几种思路 1:布隆过滤器 首先,布隆过滤器能解决绝大部分恶意攻击的请求,比如我们数据库中的id通常都设为自增的,是有一定范围大小的,如果有黑客恶意用数据库中没有的id一直访问 ...
- 高并发专题--5:关于redis高并发你晓得多少?
关于redis高并发你晓得多少? 1.redis高并发跟整个系统的高并发之间的关系 2.redis不能支撑高并发的瓶颈在哪里? 3.如果redis要支撑超过10万+的并发,那应该怎么做? 4.redi ...
- Redisson分布式锁实战(适用于Redis高并发场景)
实现方式一:存在抛异常后lock值无法归0的问题 @Autowired private StringRedisTemplate stringRedisTemplate;@RequestMapping( ...
- java redis使用卡死_jedispool连redis高并发卡死的问题
java端在使用jedispool 连接redis的时候,在高并发的时候经常死锁,或报连接异常,JedisConnectionException,或者getResource 异常等各种问题 在使用je ...
- redis高并发数据错乱_redis总结:1T以上海量数据+10万以上QPS高并发+99.99%高可用...
来源:https://blog.csdn.net/qq_34246646/article/details/104402510 redis作用 topic:高并发.亿级流量.高性能.海量数据的场景,电商 ...
- php redis auth 高并发,PHP+Redis高并发
初学Redis,于是便想写一个高并发的项目,最开始只能达到并发量1,后来增加并发,出现各种问题,我又采取各种办法来增大并发量,在慢慢增大并发量的过程中,我也在慢慢成长,在追求成功的过程中,越来越兴奋, ...
最新文章
- JS设计模式(13)状态模式
- 云计算里的安全:警惕云服务被恶意利用
- silverlight中递归构造无限级树treeview+checkbox
- 八十四、搜索框动画效果实现,React-Redux 进行应用数据的管理
- CodeIgniter模型
- DVWA--File Inclusion(文件包含)--四个级别
- mysql下一个版本号_mysql下一个版本应该且实现并不复杂增加的常用功能
- adf开发_ADF:动态视图对象
- 知识点old1908
- 程序员是吃青春饭的,为啥还有这么多人想转行当程序员?
- mysql父子节点分层_mysql 递归实例 父子节点层级递归
- Action类一般情况为何要继承ActionSupport
- Telnet 工具远程连接服务器
- mame0.239选定系统所需要的ROM/磁碟映像档为缺少或不正确
- BZOJ 2006超级钢琴
- 阿里ESC7天训练营---搭建FTP服务
- jQuery - 设置div的内容和属性
- Oracle XE版安装与用户操作
- nginx: [warn] conflicting server name “www.yqq.org“ on 0.0.0.0:80, ignored
- DB2 错误解决方案:A system temporary table space with sufficient page size does not exist.. SQLCODE=-1585,