Build a Scalable Lottery System in Java: Database, Prize Pool & Distribution
This article explains how to implement a lottery feature in a Java backend, covering database schema design for prizes, probability limits and records, constructing a prize pool, the random selection algorithm, handling prize limits, and using a factory pattern to distribute various prize types.
Overview
In many projects a lottery feature such as a spin wheel, scratch card, or slot machine is required. The backend implementation is similar for all; this article presents a common lottery implementation method.
The lottery process includes the following aspects:
Prize
Prize pool
Lottery algorithm
Prize limits
Prize distribution
Prize
Prize table
CREATE TABLE `points_luck_draw_prize` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '奖品名称',
`url` varchar(50) DEFAULT NULL COMMENT '图片地址',
`value` varchar(20) DEFAULT NULL,
`type` tinyint(4) DEFAULT NULL COMMENT '类型1:红包2:积分3:体验金4:谢谢惠顾5:自定义',
`status` tinyint(4) DEFAULT NULL COMMENT '状态',
`is_del` bit(1) DEFAULT NULL COMMENT '是否删除',
`position` int(5) DEFAULT NULL COMMENT '位置',
`phase` int(10) DEFAULT NULL COMMENT '期数',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=164 DEFAULT CHARSET=utf8mb4 COMMENT='奖品表';Prize probability limit table
CREATE TABLE `points_luck_draw_probability` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`points_prize_id` bigint(20) DEFAULT NULL COMMENT '奖品ID',
`points_prize_phase` int(10) DEFAULT NULL COMMENT '奖品期数',
`probability` float(4,2) DEFAULT NULL COMMENT '概率',
`frozen` int(11) DEFAULT NULL COMMENT '商品抽中后的冷冻次数',
`prize_day_max_times` int(11) DEFAULT NULL COMMENT '该商品平台每天最多抽中的次数',
`user_prize_month_max_times` int(11) DEFAULT NULL COMMENT '每位用户每月最多抽中该商品的次数',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb4 COMMENT='抽奖概率限制表';Prize record table
CREATE TABLE `points_luck_draw_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`member_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`member_mobile` varchar(11) DEFAULT NULL COMMENT '中奖用户手机号',
`points` int(11) DEFAULT NULL COMMENT '消耗积分',
`prize_id` bigint(20) DEFAULT NULL COMMENT '奖品ID',
`result` smallint(4) DEFAULT NULL COMMENT '1:中奖 2:未中奖',
`month` varchar(10) DEFAULT NULL COMMENT '中奖月份',
`daily` date DEFAULT NULL COMMENT '中奖日期(不包括时间)',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3078 DEFAULT CHARSET=utf8mb4 COMMENT='抽奖记录表';Prize Pool
The prize pool is assembled from prize probabilities and limits. It consists of the total pool value and the range (start and end) each prize occupies.
Total pool value: sum of all prize pool values.
Each prize's pool value: commonly calculated as probability*10000 (integer) or probability*10000 multiplied by remaining quantity.
PrizePool class
public class PrizePool implements Serializable {
/** total pool value */
private int total;
/** list of prize beans in the pool */
private List<PrizePoolBean> poolBeanList;
}PrizePoolBean class
public class PrizePoolBean implements Serializable {
/** real prize ID from DB */
private Long id;
/** start of the prize's pool range */
private int begin;
/** end of the prize's pool range */
private int end;
}Assembling the prize pool
private PrizePool getZillionairePrizePool(Map<Long, ActivityProduct> zillionaireProductMap, boolean flag) {
int total = 0;
List<PrizePoolBean> poolBeanList = new ArrayList<>();
for (Entry<Long, ActivityProduct> entry : zillionaireProductMap.entrySet()) {
ActivityProduct product = entry.getValue();
if (!flag && product.getCategoryId() == ActivityPrizeTypeEnums.XJ.getType()) {
continue;
}
PrizePoolBean prizePoolBean = new PrizePoolBean();
prizePoolBean.setId(product.getProductDescriptionId());
prizePoolBean.setBengin(total);
total = total + product.getEarnings().multiply(new BigDecimal("10000")).intValue();
prizePoolBean.setEnd(total);
poolBeanList.add(prizePoolBean);
}
PrizePool prizePool = new PrizePool();
prizePool.setTotal(total);
prizePool.setPoolBeanList(poolBeanList);
return prizePool;
}Lottery Algorithm
The algorithm works by generating a random integer within the total pool value and iterating through the prize range list to find the matching prize.
public static PrizePoolBean getPrize(PrizePool prizePool) {
int total = prizePool.getTotal();
Random rand = new Random();
int random = rand.nextInt(total);
for (PrizePoolBean prizePoolBean : prizePool.getPoolBeanList()) {
if (random >= prizePoolBean.getBengin() && random < prizePoolBean.getEnd()) {
return prizePoolBean;
}
}
return null;
}Prize Limits
Large prizes often have daily or per‑user limits. When few prizes have limits, they can be filtered during pool assembly; when many have limits, it is more efficient to validate after drawing and replace invalid prizes with a fallback.
Prize Distribution
Prize distribution can be implemented with a factory pattern, where each prize type has its own processor.
@Async("myAsync")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public Future<Boolean> sendPrize(Long memberId, List<PrizeDto> prizeList) {
try {
for (PrizeDto prizeDto : prizeList) {
if (prizeDto.getType() == PointsLuckDrawTypeEnum.XXHG.getType()) {
continue;
}
SendPrizeProcessor sendPrizeProcessor = sendPrizeProcessorFactory.getSendPrizeProcessor(
PointsLuckDrawTypeEnum.getPointsLuckDrawTypeEnumByType(prizeDto.getType()));
if (ObjectUtil.isNotNull(sendPrizeProcessor)) {
sendPrizeProcessor.send(memberId, prizeDto);
}
}
return new AsyncResult<>(Boolean.TRUE);
} catch (Exception e) {
saveSendPrizeErrorLog(memberId, prizeList);
LOGGER.error("积分抽奖发放奖品出现异常", e);
return new AsyncResult<>(Boolean.FALSE);
}
}The factory retrieves the appropriate processor based on the prize type, and a concrete example for a red‑packet prize is shown.
@Component("sendHbPrizeProcessor")
public class SendHbPrizeProcessor implements SendPrizeProcessor {
private Logger LOGGER = LoggerFactory.getLogger(SendHbPrizeProcessor.class);
@Resource
private CouponService couponService;
@Resource
private MessageLogService messageLogService;
@Override
public void send(Long memberId, PrizeDto prizeDto) throws Exception {
Coupon coupon = couponService.receiveCoupon(memberId, Long.parseLong(prizeDto.getValue()));
messageLogService.insertActivityMessageLog(memberId,
"你参与积分抽大奖活动抽中的" + coupon.getAmount() + "元理财红包已到账,谢谢参与",
"积分抽大奖中奖通知");
LOGGER.info(memberId + "在积分抽奖中抽中的" + prizeDto.getPrizeName() + "已经发放!");
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
