过年想要红包?年前你先把咱们的红包系统上线了呗!

加入“ PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

本文主要是讲述在多年前本人在某公司做红包支付系统的设计,趁着掘金的年底活动和大家一起分享。

红包分类

产品需求设计分为两类红包,个人红包,群红包。群红包又分为专属、均分、群手气三种。分别适应不同的场景。如下图所示: image.png

红包实现

发红包流程:

1、用户进入发红包界面发起请求; 2、服务端接受到请求后,对用户的红包金额进行冻结(前提用户事先开通余额账户)。 3、是否余额充足(兜底教研),如果充足发红包成功,并且生成红包记录,如果不充足提示错误信息。 4、推送最终结果给用户,如果发成功了会推两条消息,一个是发送人告诉用户红包发成功了,且推送群/个人立即领取。 5、如果红包发成功后,发一个延迟1天的 MQ 消息,做一个超期未退款处理。把冻结账户的钱返还给用户。 image.png

领红包流程:

当用户收到红包的推送过后,用户就可以通过该消息,进行红包的领取。 1、参数状态判断,判断红包是否过期,红包是否领完,红包是否重复领取等逻辑; 2、生成红包领取记录; 3、生成红包入账记录,对领取者上账。生成余额流水,并且增加余额流水。 4、减少发红包者的冻结余额,完成红包领取流程。 未命名文件.png

红包高并发

高并发设计

对于群手气红包肯定会存在并发问题,比如微信群红包领取的时候。一个 200 人的群同时来领取1个 200 元,10 人可以领取的红包,从用户的发起可能到领取基本是在 1-2 秒内领完。 怎么样保证既能高效的领取,又能保证金额都能正确,且不会记错账。我们采用的方案就是“一快一慢”的方案。 ​

1、对于群红包的场景,我们首先会将红包金额提前计算。然后存储到 Redis 中 Key : redpacket:amount_list:#{id}存储的结构是一个 List 集合。 2、 每次领取的时候我们会做一个 rpop操作,获取到红包的金额。由于这块是redis 操作,是非常高效的。 image.png 3、为了保证数据的持久性、可靠性。我们会生成一个领取记录到 mysql 数据库中持久化。 4、然后发送红包领取成功的消息出去,在记账服务中进行订阅,异步入账。

具体的流程如下: image.png

幂等性保证

为了保证领取过程用户有序的领取,且保证一个用户只能领取成功一次,如果第二次来领取,咱们就提示已经领取过了,不能重复领取。这是我们在高并发场景下必须严保证的问题。当时我们选择的是通过分布式锁的方式来解决的,锁的 key 设计如下: redpacket:amount_list:#{id}_#{user_id} 这样设计的好处就是能够保证一当前分布式系统中,当前只能一个有效的请求进入正真的处理逻辑中。 兜底保障: 1、在领取记录表中增加 user_id, redpacket_id 为唯一索引。 2、对红包的剩余金额做乐观锁更新(可以使用 tk.mapper 的 @Version)。

可拓展性设计

为了保证可拓展性的设计,我们当时采用的是 策略 + 模板方法的设计模型进行低耦合设计。

手气红包金额计算

我们采用的是中位数随机算法(大致的逻辑就是控制一个中位数的值最大金额,最小金额的区间不能超过中位数的浮动水位线),更多的随机算法,大家可以参阅:为啥春节抢红包总不是手气最佳?看完微信抢红包算法你就明白了!

金额随机代码

public class RedPacketUtils {

	private static final Random random = new Random();


	/**
	 * 根据总数分割个数及限定区间进行数据随机处理
	 * 数列浮动阀值为0.95
	 *
	 * @param totalMoney - 被分割的总数
	 * @param splitNum   - 分割的个数
	 * @param min        - 单个数字下限
	 * @param max        - 单个数字上限
	 * @return - 返回符合要求的数字列表
	 */
	public static List<BigDecimal> genRandomList(BigDecimal totalMoney, Integer splitNum, BigDecimal min, BigDecimal max) {
		totalMoney = totalMoney.multiply(new BigDecimal(100));
		min = min.multiply(new BigDecimal(100));
		max = max.multiply(new BigDecimal(100));
		List<Integer> li = genRandList(totalMoney.intValue(), splitNum, min.intValue(), max.intValue(), 0.95f);
		List<BigDecimal> randomList = new CopyOnWriteArrayList<>();
		for (Integer v : li) {
			BigDecimal randomVlue = new BigDecimal(v).divide(new BigDecimal(100));
			randomList.add(randomVlue);
		}

		randomList = randomArrayList(randomList);
		return randomList;
	}

	/**
	 * 根据总数分割个数及限定区间进行数据随机处理
	 *
	 * @param total    - 被分割的总数
	 * @param splitNum - 分割的个数
	 * @param min      - 单个数字下限
	 * @param max      - 单个数字上限
	 * @param thresh   - 数列浮动阀值[0.0, 1.0]
	 */
	public static List<Integer> genRandList(int total, int splitNum, int min, int max, float thresh) {
		assert total >= splitNum * min && total <= splitNum * max : "请校验红包参数设置的合理性";
		assert thresh >= 0.0f && thresh <= 1.0f;
		// 平均分配
		int average = total / splitNum;
		List<Integer> list = new ArrayList<>(splitNum);
		int rest = total - average * splitNum;
		for (int i = 0; i < splitNum; i++) {
			if (i < rest) {
				list.add(average + 1);
			} else {
				list.add(average);
			}
		}
		// 如果浮动阀值为0则不进行数据随机处理
		if (thresh == 0) {
			return list;
		}
		// 根据阀值进行数据随机处理
		int randOfRange = 0;
		int randRom = 0;
		int nextIndex = 0;
		int nextValue = 0;
		int surplus = 0;//多余
		int lack = 0;//缺少
		for (int i = 0; i < splitNum - 1; i++) {
			nextIndex = i + 1;
			int itemThis = list.get(i);
			int itemNext = list.get(nextIndex);
			boolean isLt = itemThis < itemNext;
			int rangeThis = isLt ? max - itemThis : itemThis - min;
			int rangeNext = isLt ? itemNext - min : max - itemNext;
			int rangeFinal = (int) Math.ceil(thresh * (Math.min(rangeThis, rangeNext) + 100));
			randOfRange = random.nextInt(rangeFinal);
			randRom = isLt ? 1 : -1;
			int iValue = list.get(i) + randRom * randOfRange;
			nextValue = list.get(nextIndex) + randRom * randOfRange * -1;
			if (iValue > max) {
				surplus += (iValue - max);
				list.set(i, max);
			} else if (iValue < min) {
				list.set(i, min);
				lack += (min - iValue);
			} else {
				list.set(i, iValue);
			}
			list.set(nextIndex, nextValue);
		}
		if (nextValue > max) {
			surplus += (nextValue - max);
			list.set(nextIndex, max);
		}
		if (nextValue < min) {
			lack += (min - nextValue);
			list.set(nextIndex, min);
		}
		if (surplus - lack > 0) {//钱发少了 给低于max的凑到max
			for (int i = 0; i < list.size(); i++) {
				int value = list.get(i);
				if (value < max) {
					int tmp = max - value;
					if (surplus >= tmp) {
						surplus -= tmp;
						list.set(i, max);
					} else {
						list.set(i, value + surplus);
						return list;
					}
				}
			}
		} else if (lack - surplus > 0) {//钱发多了 给超过高于min的人凑到min
			for (int i = 0; i < list.size(); i++) {
				int value = list.get(i);
				if (value > min) {
					int tmp = value - min;
					if (lack >= tmp) {
						lack -= tmp;
						list.set(i, min);
					} else {
						list.set(i, min + tmp - lack);
						return list;
					}
				}
			}
		}
		return list;
	}

	/**
	 * 打乱ArrayList
	 */
	public static List<BigDecimal> randomArrayList(List<BigDecimal> sourceList) {
		if (sourceList == null || sourceList.isEmpty()) {
			return sourceList;
		}
		List<BigDecimal> randomList = new CopyOnWriteArrayList<>();
		do {
			int randomIndex = Math.abs(new Random().nextInt(sourceList.size()));
			randomList.add(sourceList.remove(randomIndex));
		} while (sourceList.size() > 0);

		return randomList;
	}


	public static void main(String[] args) {
		Long startTi = System.currentTimeMillis();
		List<BigDecimal> li = genRandomList(new BigDecimal(100000), 26000, new BigDecimal(2), new BigDecimal(30));
		li = randomArrayList(li);
		BigDecimal total = BigDecimal.ZERO;
		System.out.println("======total=========total:" + total);
		System.out.println("======size=========size:" + li.size());
		Long endTi = System.currentTimeMillis();
		System.out.println("======耗时=========:" + (endTi - startTi) / 1000 + "秒");
	}
}
复制代码

参考文档

猜你喜欢

转载自juejin.im/post/7054561013839953956