抢红包
红包算法
1.需要几个参数:红包金额(总积分)、拆分红包个数、红包最小金额、最大金额、偏移量。
/**
* 红包生成算法
* @Author: fh
*/
public class RedEnveLoeRandomUtil {
/**
* 返回一次抽奖在指定中奖概率下是否中奖
* @param rate 中奖概率
*/
private static boolean canReward(double rate) {
return Math.random() <= rate;
}
/**
* 返回min~max区间内随机数,含min和max
*/
private static int getRandomVal(int min, int max) {
Random random = new Random();
return random.nextInt(max - min + 1) + min;
}
/**
* 带概率偏向的随机算法,概率偏向subMin~subMax区间
* 返回boundMin~boundMax区间内随机数(含boundMin和boundMax),同时可以指定子区间subMin~subMax的优先概率
* 例:传入参数(10, 50, 20, 30, 0.8),则随机结果有80%概率从20~30中随机返回,有20%概率从10~50中随机返回
*/
private static int getRandomValWithSpecifySubRate(int boundMin, int boundMax, int subMin, int subMax, double subRate) {
if (canReward(subRate)) {
return getRandomVal(subMin, subMax);
}
return getRandomVal(boundMin, boundMax);
}
/**
* 随机分配第n个红包
* @param totalBonus 总红包量
* @param totalNum 总份数
* @param sendedBonus 已发送红包量
* @param sendedNum 已发送份数
* @param rdMin 随机下限
* @param rdMax 随机上限
* @return
*/
private static Integer randomBonusWithSpecifyBound(Integer totalBonus, Integer totalNum, Integer sendedBonus,
Integer sendedNum, Integer rdMin, Integer rdMax, double bigRate) {
Integer avg = totalBonus / totalNum; // 平均值
Integer leftLen = avg - rdMin;
Integer rightLen = rdMax - avg;
Integer boundMin = 0, boundMax = 0;
// 大范围设置小概率
if (leftLen.equals(rightLen)) {
boundMin = Math.max((totalBonus - sendedBonus - (totalNum - sendedNum - 1) * rdMax), rdMin);
boundMax = Math.min((totalBonus - sendedBonus - (totalNum - sendedNum - 1) * rdMin), rdMax);
} else if (rightLen.compareTo(leftLen) > 0) {
// 上限偏离
Integer standardRdMax = avg + leftLen; // 右侧对称上限点
Integer _rdMax = canReward(bigRate) ? rdMax : standardRdMax;
boundMin = Math.max((totalBonus - sendedBonus - (totalNum - sendedNum - 1) * standardRdMax), rdMin);
boundMax = Math.min((totalBonus - sendedBonus - (totalNum - sendedNum - 1) * rdMin), _rdMax);
} else {
// 下限偏离
Integer standardRdMin = avg - rightLen; // 左侧对称下限点
Integer _rdMin = canReward(bigRate) ? rdMin : standardRdMin;
boundMin = Math.max((totalBonus - sendedBonus - (totalNum - sendedNum - 1) * rdMax), _rdMin);
boundMax = Math.min((totalBonus - sendedBonus - (totalNum - sendedNum - 1) * standardRdMin), rdMax);
}
// 已发平均值偏移修正-动态比例
if (boundMin.equals(boundMax)) {
return getRandomVal(boundMin, boundMax);
}
double currAvg = sendedNum == 0 ? (double)avg : (sendedBonus / (double)sendedNum); // 当前已发平均值
double middle = (boundMin + boundMax) / 2.0;
Integer subMin = boundMin, subMax = boundMax;
// 期望值
double exp = avg - (currAvg - avg) * sendedNum / (double)(totalNum - sendedNum);
if (middle > exp) {
subMax = (int) Math.round((boundMin + exp) / 2.0);
} else {
subMin = (int) Math.round((exp + boundMax) / 2.0);
}
Integer expBound = (boundMin + boundMax) / 2;
Integer expSub = (subMin + subMax) / 2;
double subRate = (exp - expBound) / (double)(expSub - expBound);
return getRandomValWithSpecifySubRate(boundMin, boundMax, subMin, subMax, subRate);
}
/**
* 生成红包一次分配结果
* @param totalBonus
* @param totalNum
* @param rdMin
* @param rdMax
* @param bigRate 指定大范围区间的概率
* @return
*/
public static List<Integer> createBonusList(Integer totalBonus, Integer totalNum, Integer rdMin, Integer rdMax, double bigRate) {
Integer sendedBonus = 0;
Integer sendedNum = 0;
List<Integer> bonusList = new ArrayList<>();
while (sendedNum < totalNum) {
Integer bonus = randomBonusWithSpecifyBound(totalBonus, totalNum, sendedBonus, sendedNum, rdMin, rdMax, bigRate);
bonusList.add(bonus);
sendedNum++;
sendedBonus += bonus;
}
return bonusList;
}
public static void main(String[] args) {
List<Integer> bonusList = createBonusList(100000, 10, 100, 100000, 0.2);
int sum = 0;
for (Integer integer : bonusList) {
System.out.println(integer);
sum += integer;
}
System.out.println("总积分"+sum);
}
}
实现思路
1.准备定时任务生成红包数据
每周定时生成红包数据,维护一张大红包表(红包发送个数、金额、金额随机大小区间就是上面红包算法的相关参数,还有红包的开始和结束的时间,当前的记录的状态可用,已使用,过期,已抢个数)在后台可以查看当前红包的状态,消费使用情况。
2.生成红包的同时,需要按第一步红包的相关信息来拆红包数据。
调用上面的红包算法,拆分成对应个数的小红包,存入红包明细表中(用户id,红包积分、上面的红包表id为了区分那个大红包的)
3.以大红包id为key,准备2个redis list队列(已消费队列,未消费队列)
将上面大红包拆分的小红包,以大红包id为key,leftpushAll 加入到未消费队列中(对应的就是我们抢红包的金额,其实都是提前算好的)
4.开始抢红包
传入用户id和大红包id,查到大红包记录。验证大红包信息,是否存在、时间是否开启、或者结束。
此处使用lua脚本来操作,保证消费队列,插入已消费队列2步的原子性。
抢的时候先从map 中验证用户是否抢过,如果存在就返回一个json串(用户id,积分,状态:已存在)。
否则,就从未消费的队列中从右边取一个小红包,如果有值说明,红包没有消费完,没有就说明消费完了返回一个抢完了就状态即可。
有值,就把用户id,积分,还有成功状态封装转换成json返回,期间还需要在map中记录上用户id,和积分。 HSET myhash userid 100 。把json 存到已消费队列。
以上就是整个的抢红包操作。
--[[ 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
参数:红包队列名, 已消费的队列名,去重的Map名,用户ID
返回值:nil 或者 json字符串,包含用户ID:userId,红包ID:id,红包金额:money
--]]
if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then
local res = "{\"status\":\"EXISTS\"}";
local resJson = cjson.decode(res);
local point = redis.call('hget', KEYS[3], KEYS[4]);
resJson['point'] = point;
local r = cjson.encode(resJson);
return r;
else
-- 先取出一个小红包
local hongBao = redis.call('rpop', KEYS[1]);
if hongBao then
local x = cjson.decode(hongBao);
-- 加入用户ID信息
x['userId'] = KEYS[4];
x['status'] = 'SUCCESS';
local re = cjson.encode(x);
-- 把用户ID放到去重的set里, value存积分
local point = x['point'];
redis.call('hset', KEYS[3], KEYS[4], point);
-- 把红包放到已消费队列里
redis.call('lpush', KEYS[2], re);
return re;
else
-- 红包已被抢完
return "{\"status\":\"NOT_ENOUGH\"}"
end
end
5.定时任务去发送积分。