Build a High‑Performance Lottery System with Spring Boot, MyBatis‑Plus & Redis
This tutorial walks through creating a Spring Boot lottery application that caches activity, prize, and record data in Redis, covering project setup, Maven dependencies, YML configuration, MyBatis‑Plus code generation, Redis key management, lottery API, event‑driven data loading, stock handling, and asynchronous record saving.
1. Project Introduction
This is a simple example based on Spring Boot, MyBatis‑Plus and Redis. Activity content, prize information and draw records are cached in Redis, and the entire lottery process operates on data stored in Redis.
2. Project Demo
Below are screenshots of the running project.
3. Table Structure
The project contains four tables: activity, prize, award and draw‑record. The SQL statements are provided at the end of the article.
4. Project Setup
4.1 Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- other dependencies omitted for brevity -->
</dependencies>4.2 YML Configuration
server:
port: 8080
servlet:
context-path: /
spring:
datasource:
druid:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 30
max-active: 100
min-idle: 10
max-wait: 60000
validation-query: SELECT 1 FROM DUAL
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: -1
max-idle: 2000
max-wait: -1
min-idle: 1
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
auto-mapping-behavior: full
mapper-locations: classpath*:mapper/**/*Mapper.xml
async:
executor:
thread:
core-pool-size: 6
max-pool-size: 12
queue-capacity: 100000
name-prefix: lottery-service-4.3 Code Generation
public class MybatisPlusGeneratorConfig {
public static void main(String[] args) {
AutoGenerator mpg = new AutoGenerator();
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("chen");
gc.setOpen(false);
gc.setSwagger2(false);
mpg.setGlobalConfig(gc);
// data source, package, strategy, template configuration omitted for brevity
mpg.execute();
}
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入" + tip + ":");
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
}4.4 Redis Configuration
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}4.5 Constant Management
public class LotteryConstants {
public static final String DRAWING = "DRAWING";
public static final String LOTTERY = "LOTTERY";
public static final String LOTTERY_PRIZE = "LOTTERY_PRIZE";
public static final String DEFAULT_LOTTERY_PRIZE = "DEFAULT_LOTTERY_PRIZE";
public enum PrizeTypeEnum { THANK(-1), NORMAL(1), UNIQUE(2); private int value; PrizeTypeEnum(int v){this.value=v;} public int getValue(){return value;} }
// other constants omitted for brevity
}4.6 Business Code
4.6.1 Lottery API
@GetMapping("/{id}")
public ResultResp<LotteryItemVo> doDraw(@PathVariable("id") Integer id, HttpServletRequest request) {
String accountIp = CusAccessObjectUtil.getIpAddress(request);
ResultResp<LotteryItemVo> resp = new ResultResp<>();
try {
checkDrawParams(id, accountIp);
DoDrawDto dto = new DoDrawDto();
dto.setAccountIp(accountIp);
dto.setLotteryId(id);
lotteryService.doDraw(dto);
resp.setCode(ReturnCodeEnum.SUCCESS.getCode());
resp.setMsg(ReturnCodeEnum.SUCCESS.getMsg());
resp.setResult(lotteryConverter.dto2LotteryItemVo(dto));
} catch (Exception e) {
return ExceptionUtil.handlerException4biz(resp, e);
} finally {
redisTemplate.delete(RedisKeyManager.getDrawingRedisKey(accountIp));
}
return resp;
}The API uses Redis SETNX to prevent duplicate draws from the same user.
4.6.2 Data Initialization (Event‑Driven)
public class InitPrizeToRedisEvent extends ApplicationEvent {
private Integer lotteryId;
private CountDownLatch countDownLatch;
public InitPrizeToRedisEvent(Object source, Integer lotteryId, CountDownLatch latch) {
super(source);
this.lotteryId = lotteryId;
this.countDownLatch = latch;
}
// getters and setters omitted
} @Component
public class InitPrizeToRedisListener implements ApplicationListener<InitPrizeToRedisEvent> {
@Autowired RedisTemplate redisTemplate;
@Autowired LotteryPrizeMapper prizeMapper;
@Autowired LotteryItemMapper itemMapper;
@Override
public void onApplicationEvent(InitPrizeToRedisEvent ev) {
Boolean first = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getLotteryPrizeRedisKey(ev.getLotteryId()), "1");
if (!first) { ev.getCountDownLatch().countDown(); return; }
// load items and prizes from DB and store them as HASHes in Redis
// ... omitted for brevity ...
ev.getCountDownLatch().countDown();
}
}4.6.3 Lottery Core Logic
private LotteryItem doPlay(Lottery lottery) {
List<LotteryItem> items = (List<LotteryItem>) redisTemplate.opsForValue().get(RedisKeyManager.getLotteryItemRedisKey(lottery.getId()));
if (items == null) { items = lotteryItemMapper.selectList(new QueryWrapper<>().eq("lottery_id", lottery.getId())); }
if (items.isEmpty()) { throw new BizException(...); }
Collections.shuffle(items);
Map<Integer, int[]> scope = new HashMap<>();
int last = 0;
for (LotteryItem it : items) {
int cur = last + new BigDecimal(it.getPercent()).multiply(new BigDecimal(1000)).intValue();
scope.put(it.getId(), new int[]{last + 1, cur});
last = cur;
}
int lucky = new Random().nextInt(1000);
int prizeId = 0;
for (Map.Entry<Integer, int[]> e : scope.entrySet()) {
if (lucky >= e.getValue()[0] && lucky <= e.getValue()[1]) { prizeId = e.getKey(); break; }
}
for (LotteryItem it : items) { if (it.getId() == prizeId) return it; }
return null;
}4.6.4 Stock Adjustment and Record
public class HasStockRewardProcessor extends AbstractRewardProcessor {
@Override
protected void processor(RewardContext ctx) {
Long left = redisTemplate.opsForHash().increment(ctx.getKey(), "validStock", -1);
if (left.intValue() < 0) { throw new UnRewardException(...); }
List<Object> props = redisTemplate.opsForHash().multiGet(ctx.getKey(), Arrays.asList("id", "prizeName"));
ctx.setPrizeId(Integer.parseInt(props.get(0).toString()));
ctx.setPrizeName(props.get(1).toString());
lotteryPrizeMapper.updateValidStock(ctx.getPrizeId());
}
@Override
protected void afterProcessor(RewardContext ctx) {
asyncLotteryRecordTask.saveLotteryRecord(ctx.getAccountIp(), ctx.getLotteryItem(), ctx.getPrizeName());
}
// getAwardType omitted
}4.6.5 Asynchronous Record Saving
@Component
public class AsyncLotteryRecordTask {
@Autowired LotteryRecordMapper recordMapper;
@Async("lotteryServiceExecutor")
public void saveLotteryRecord(String ip, LotteryItem item, String prizeName) {
LotteryRecord rec = new LotteryRecord();
rec.setAccountIp(ip);
rec.setItemId(item.getId());
rec.setPrizeName(prizeName);
rec.setCreateTime(LocalDateTime.now());
recordMapper.insert(rec);
}
}4.7 Summary
The article demonstrates how to build a complete lottery system, from front‑end interaction to back‑end implementation, using Spring Boot, MyBatis‑Plus, Redis, event‑driven data loading, distributed locking, and asynchronous processing.
5. Project Repository
https://gitee.com/cl1429745331/redis-demo
Remember to adjust the activity end time in the database before running the project.
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 Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
