微信群红包发起和领取接口设计v1.0

73 阅读8分钟

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

微信群红包的一般设计思路:

1.用户A进入微信群,点击“发送红包”按钮,调用后台接口向所有人发放总金额为M元的红包,填写必要的参数,如红包个数、祝福语等。

2.后台接收到发送红包请求后,根据红包总金额和红包个数,调用类似上述的红包拆分算法计算出每个红包的具体金额。

3.后台将红包和每个红包对应的金额保存到数据库中,并生成一个唯一的红包ID(例如UUID),将红包ID返回给用户A4.用户A将红包ID分享给微信群内其他人,其他人可以通过红包ID领取红包。

5.当有用户B点击领取红包时,调用后台接口向后台查询是否存在该红包ID,如果存在,则将该红包的金额加入到用户B的余额中,并修改数据库中该红包的状态为已领取。

6.当所有红包都被领取完毕后,后台统计红包领取情况并计算出瓜分金额。

7.后台调用类似上述的红包拆分算法计算出每个人应该获得的瓜分金额。

8.后台将瓜分金额和每个参与者对应的用户ID返回给微信客户端。

9.微信客户端根据后台返回的瓜分金额,调用微信支付接口向每个参与者转账。

10.转账完成后,微信客户端显示瓜分结果,并提示用户查看余额变动情况。

本文章仅实现用户发起群红包以及用户抢红包的接口。

数据库表:

红包表(red_packet):

字段名类型描述
idvarchar(36)红包ID,唯一标识
total_amountdecimal(10,2)红包总金额
total_countsint红包总个数
create_timedatetime创建时间
update_timedatetime更新时间

红包明细表(red_packet_item):

字段名类型描述
idvarchar(36)明细项ID,唯一标识
red_packet_idvarchar(36)红包ID
amountdecimal(10,2)当前明细项金额
is_grabbedtinyint(1)是否已领取(0-未领取;1-已领取)
grab_user_idvarchar(36)领取用户ID
grab_timedatetime领取时间

这里使用Jpa来实现实体类:

/**
* 红包表
*/
@Data
@Entity
@Table(name = "red_packet")
public class RedPacket {
    @Id
    private String id;

    @Column(name = "total_amount")
    private BigDecimal totalAmount;

    @Column(name = "total_counts")
    private int totalCounts;

    @Column(name = "create_time")
    private LocalDateTime createTime;

    @Column(name = "update_time")
    private LocalDateTime updateTime;
}
/**
* 红包明细表
*/
@Data
@Entity
@Table(name = "red_packet_item")
public class RedPacketItem {
    @Id
    private String id;

    @Column(name = "red_packet_id")
    private String redPacketId;

    @Column(name = "amount")
    private BigDecimal amount;

    @Column(name = "is_grabbed")
    private boolean isGrabbed;

    @Column(name = "grab_user_id")
    private String grabUserId;

    @Column(name = "grab_time")
    private LocalDateTime grabTime;
}

创建JPA Repository接口,用于查询和操作数据库:

@Repository
public interface RedPacketRepository extends JpaRepository<RedPacket, String> {
}
@Repository
public interface RedPacketItemRepository extends JpaRepository<RedPacketItem, String> {
    //取出一个还没领过的红包:
    RedPacketItem findFirstByRedPacketIdAndIsGrabbed(String redPacketId, boolean isGrabbed);
}

controller层拆分和领取红包的接口:

@Controller 
@RequestMapping("/redPacket") 
public class RedPacketController { 

    @Autowired 
    private RedPacketService redPacketService;
    /**
     * 发送红包
     * @param totalAmount 红包总金额
     * @param totalCounts 红包总个数
     * @return String 红包ID
     */
    @Slf4j
    @PostMapping("/sendRedPacket")
    public String sendRedPacket(@RequestParam("totalAmount") BigDecimal totalAmount,
                                @RequestParam("totalCounts") int totalCounts) {
        String redPacketId = redPacketService.sendRedPacket(totalAmount, totalCounts);
        log.info("发送红包成功,红包ID为:{}",redPacketId);
        return redPacketId;
    }

    /**
     * 领取红包
     * @param userId 用户ID
     * @paramredPacketId 红包ID
     * @return BigDecimal 领取金额
     */
    @PostMapping("/grabRedPacket")
    public BigDecimal grabRedPacket(@RequestParam("userId") String userId,
                                    @RequestParam("redPacketId") String redPacketId) {
        BigDecimal amount = redPacketService.grabRedPacket(userId, redPacketId);
        log.info("领取红包信息:userId => {} , redPacketId => {} ",userId,redPacketId);
        return amount;
    }

service层服务类代码:

@Service
public class RedPacketService {
    //保证公平性和随机性
    private static AtomicLong timestampGenerator = new AtomicLong();
    // 红包抢占锁前缀:
    private static final String LOCK_PREFIX = "lock:redPacket:";  
    // 红包明细项抢占锁前缀:
    private static final String ITEM_LOCK_PREFIX = "lock:redPacketItem:"; 

    @Autowired
    private RedPacketRepository redPacketRepository;

    @Autowired
    private RedPacketItemRepository redPacketItemRepository;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    /**
     * 发送红包
     *
     * @param totalAmount 红包总金额
     * @param totalCounts 红包总个数
     * @return String 红包ID
     */
    @Transactional
    public String sendRedPacket(BigDecimal totalAmount, int totalCounts) {
        // 创建红包记录
        RedPacket redPacket = new RedPacket();
        redPacket.setId(UUID.randomUUID().toString());
        redPacket.setTotalAmount(totalAmount);
        redPacket.setTotalCounts(totalCounts);
        LocalDateTime now = LocalDateTime.now();
        redPacket.setCreateTime(now);
        redPacket.setUpdateTime(now);
        redPacketRepository.save(redPacket);

        // 拆分红包,生成明细项,并保存到数据库
        List<BigDecimal> amounts = splitRedPacket(totalAmount, totalCounts);
        //保存红包拆分明细项:
        saveRedPacketItems(redPacket.getId(), amounts);

        // 返回红包ID给用户A
        return redPacket.getId();
    }

    /**
     * 领取红包
     *
     * @param userId      用户ID
     * @param redPacketId 红包ID
     * @return BigDecimal 领取金额
     */
    public BigDecimal grabRedPacket(String userId, String redPacketId) {
        String lockKey = LOCK_PREFIX + redPacketId;
        String requestId = UUID.randomUUID().toString();
        // 获取锁:
        RLock lock = redissonClient.getLock(lockKey); 
        boolean isLocked = false;
        try {
            // 尝试获取锁,设置锁定时间和等待时间:
            isLocked = lock.tryLock(5L, 10L, TimeUnit.SECONDS); 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        if (!isLocked) {
            throw new RuntimeException("领取失败,请稍后再试!");
        }

        try {
            //获取未领取的红包明细列表:
            List<String> itemIds = redPacketItemRepository
            .findIdsByRedPacketIdAndIsGrabbed(redPacketId, false);
            //判断核心数:
            int poolSize = taskExecutor.getCorePoolSize();
            //红包明细项个数 < 核心数,走同步:
            if (itemIds.size() < poolSize * 2) {
                return grabRedPacketSync(userId, itemIds);
            //走异步:    
            } else {
                return grabRedPacketAsync(userId, itemIds);
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    /**
     * 拆分红包,返回每个人的金额列表
     *
     * @param totalAmount 红包总金额
     * @param totalCounts 红包总个数
     * @return List<BigDecimal> 每个人的金额列表
     */
    private List<BigDecimal> splitRedPacket(BigDecimal totalAmount, int totalCounts) {
        //最少0.01元:
        BigDecimal minAmount = new BigDecimal("0.01");
        // 将总金额除以总数量,得到每个红包的平均值:
        BigDecimal average = totalAmount.divide(BigDecimal.valueOf(totalCounts), 2, RoundingMode.HALF_UP);
        //最大值(手气最佳):
        BigDecimal maxAmount = average.multiply(BigDecimal.valueOf(2));
        List<BigDecimal> amounts = new ArrayList<>(totalCounts);

        // 使用Stream API生成每个人的金额
        for (int i = 0; i < totalCounts - 1; i++) {
            BigDecimal splitPoint = new BigDecimal(ThreadLocalRandom.current().nextInt(
                    minAmount.multiply(BigDecimal.valueOf(100)).intValue(),
                    maxAmount.multiply(BigDecimal.valueOf(100)).intValue() + 1))
                    .multiply(new BigDecimal("0.01"));
            amounts.add(splitPoint);
            maxAmount = average.multiply(BigDecimal.valueOf(2)).subtract(splitPoint);
        }
        //计算每个份额的金额,并将其添加到结果列表中:
        amounts.add(totalAmount.subtract(amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add)));

        // 打乱顺序,避免每个人都领到相同的金额
        Collections.shuffle(amounts);
        return amounts;
    }

    /**
     * 将明细项保存到数据库中
     *
     * @param redPacketId 红包ID
     * @param amounts     每个人的金额列表
     */
     private void saveRedPacketItems(String redPacketId, List<BigDecimal> amounts) {
        LocalDateTime now = LocalDateTime.now();
        List<RedPacketItem> items = amounts.stream().map(amount -> {
            RedPacketItem item = new RedPacketItem();
            item.setId(UUID.randomUUID().toString());
            item.setRedPacketId(redPacketId);
            item.setAmount(amount);
            item.setIsGrabbed(false);
            // 使用AtomicLong类型的变量获取唯一的时间戳:(在高并发的情况下,如果时间戳生成过快可能会出现溢出问题。由于Java中用于表示时间戳的`long`类型是一个64位有符号整数,其范围为-2^63 ~ 2^63-1,如果程序每毫秒生成的数据量超过2^63/1000=9223372036854条,就会导致时间戳溢出。因为红包数量太少,所以不会有溢出的问题)
            item.setCreateTime(timestampGenerator.incrementAndGet()); 
            item.setUpdateTime(now);
            return item;
        }).collect(Collectors.toList());
        redPacketItemRepository.saveAll(items);
    }

    /**
     * 同步方法执行抢红包操作
     *
     * @param userId  用户ID
     * @param itemIds 未被领取的红包明细项ID列表
     * @return BigDecimal 领取金额
     */
     private BigDecimal grabRedPacketSync(String userId, List<String> itemIds) {
        // 获取未领取的红包:
        List<RedPacketItem> itemList = redPacketItemRepository
        .findByIdInAndIsGrabbed(itemIds, false,Sort.by(Sort.Direction.ASC, "createTime"));

        if (itemList == null || itemList.isEmpty()) {
            throw new RuntimeException("红包已经被抢完啦!");
        }

        // 抢红包,修改明细项状态和记录抢红包用户信息
        RedPacketItem redPacketItem = itemList.get(0);
        redPacketItem.setIsGrabbed(true);
        redPacketItem.setGrabUserId(userId);
        redPacketItem.setGrabTime(LocalDateTime.now());
        redPacketItemRepository.save(redPacketItem);

        // 返回抢到的红包金额
        return redPacketItem.getAmount();
    }

    /**
     * 异步方法执行抢红包操作
     *
     * @param userId  用户ID
     * @param itemIds 未被领取的红包明细项ID列表
     * @return BigDecimal 领取金额
     */
    private BigDecimal grabRedPacketAsync(String userId, List<String> itemIds) {
        int size = itemIds.size();
        CountDownLatch countDownLatch = new CountDownLatch(size);
        List<BigDecimal> amounts = new ArrayList<>(size); // 用来存储每个用户抢到的金额

        for (String id : itemIds) {
            taskExecutor.submit(() -> {
                String lockKey = ITEM_LOCK_PREFIX + id;
                String requestId = UUID.randomUUID().toString();
                // 获取锁:
                RLock lock = redissonClient.getLock(lockKey);
                boolean isLocked = false;
                try {
                    // 尝试获取锁,在5秒内没有获取到锁,则放弃获取并返回false,获取到锁后,10秒内还没释放锁则自动释放锁:
                    isLocked = lock.tryLock(5L, 10L, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                if (!isLocked) { // 如果获取锁失败,直接返回异常信息
                    throw new RuntimeException("领取失败,请稍后再试!");
                }

                try {
                    RedPacketItem item = redPacketItemRepository.findByIdAndIsGrabbed(id, false);
                    if (item != null) {
                        item.setIsGrabbed(true);
                        item.setGrabUserId(userId);
                        item.setGrabTime(LocalDateTime.now());
                        //将userId入库,表示该用户抢到该红包:
                        redPacketItemRepository.save(item);

                        BigDecimal amount = item.getAmount();
                        amounts.add(amount); // 将当前用户抢到的金额加入列表
                    }
                } finally {
                    lock.unlock(); // 释放锁
                    countDownLatch.countDown();
                }
            });
        }

        try {
            countDownLatch.await(); // 等待所有异步任务执行完成
        } catch (InterruptedException e) {
            //避免由于线程阻塞或中断引起的各种问题和风险:
            Thread.currentThread().interrupt();
        }

        if (amounts.isEmpty()) { // 如果没有用户成功抢到红包,则返回空值
            return null;
        } else { // 如果有用户成功抢到红包,则返回当前用户抢到的金额
            return amounts.get(0);
        }
    }


}

代码中的一些细节注意项:

  1. 这里加上了根据时间排序保证红包抢夺的公平性和随机性,避免线程饥饿。因为红包抢夺行为本身具有随机性,而通过按照时间排序的方式,可以确保先抢到红包的用户是先进入系统的用户,这样可以增加后进入系统的用户抢到红包的概率,提高系统的公平性和随机性。 另外,采用按照时间排序的方式,还可以避免可能出现的线程饥饿问题。如果不进行排序,那么一些在系统中等待的用户可能会因为竞争不到资源而永远无法抢到红包,从而导致线程饥饿的问题。
//保证创建时间戳唯一:
private static AtomicLong timestampGenerator = new AtomicLong();
//提高公平性,避免线程饥饿:
List<RedPacketItem> itemList = redPacketItemRepository
        .findByIdInAndIsGrabbed(itemIds, false,Sort.by(Sort.Direction.ASC, "createTime"));
  1. 领红包分为同步和异步方法,因为如果红包明细项比线程池处理器数量少,那么使用同步方法进行抢红包操作更为合适。因为同步方法会阻塞线程,等待其他线程完成自己的任务后再执行,这样可以避免多个线程同时访问同一个资源而导致的问题,如数据不一致或者竞争条件等。而如果明细项比线程池处理器数量多,采用异步方法执行抢红包操作效率会更高。因为异步方法会将任务提交给线程池中的某一个空闲线程执行,而不是直接阻塞当前线程等待任务完成。这样可以提高系统的响应速度和并发能力,优化系统的资源利用效率。同时,异步方法还可以减少线程上下文切换的开销,从而提高系统的性能表现。

  2. 对于同步领红包方法grabRedPacketSync()只需要在进入该方法前进行判断锁是否占有即可。而对于异步方法grabRedPacketAsync()则是进入该方法前和该方法中进行判断锁是否占有。当第一个请求进入grabRedPacketAsync()方法时,会使用lock.tryLock(5L, 10L, TimeUnit.SECONDS);方法尝试在5秒内获取到锁,获取不到则直接返回领取失败,获取到了则进行抢红包操作;抢红包操作完成后,调用lock.unlock();方法释放锁,表示当前请求执行完毕,其他请求可以继续进入抢红包操作。通过在方法进入前和离开后加上同样的分布式锁判断,能够确保每个请求都能够安全地进行抢红包操作,并避免出现数据异常或重复操作等问题。

以上就是使用了线程池和异步的方式来提高抢红包接口的并发性。但以上也还是有能优化的余地,比如将红包明细进行缓存,再通过消息中间件的方式异步入库,可以进一步提高接口的并发性和响应。

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍