抢红包

红包算法

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.定时任务去发送积分。