redis高并发抽奖

来源:互联网 发布:淘宝代运营公司w863 编辑:程序博客网 时间:2024/06/05 22:56

最近一直在忙,只能抽空周末把代码撸了出来。周一才来写这篇文章。代码有点缭乱,没时间整理,如果有误还请留言斧正。现在进入正题。

一、思路

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;}@Overridepublic 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();@Overridepublic 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);                                      //获取中奖奖品IDjedis.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被改动了,则返回nullif (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存储验证)。--验证通过。

以上就是全部内容了,有点糙,还望见谅。

 

原创粉丝点击