Backend Development 32 min read

Spring Boot Lottery System with Redis and MyBatis-Plus: Project Overview and Implementation Guide

This article presents a step‑by‑step guide to building a Spring Boot lottery application that leverages MyBatis‑Plus and Redis for caching activity, prize, and record data, covering project setup, dependencies, configuration, code generation, Redis integration, lottery logic, and stock management.

Java Captain
Java Captain
Java Captain
Spring Boot Lottery System with Redis and MyBatis-Plus: Project Overview and Implementation Guide

1. Project Introduction

This is a simple example based on Spring Boot, MyBatis‑Plus and Redis.

The activity content, prize information and record information are cached in Redis, and the entire lottery process operates on data stored in Redis.

The overall content is straightforward; the specific operations are analyzed below.

2. Project Demo

First, take a look at the screenshots of the project effect. If it looks acceptable, we will examine the implementation details.

3. Database Schema

The project contains four tables: activity, prize, award and winning‑record tables. The specific SQL statements are provided at the end of the article.

4. Project Setup

First, create a standard Spring Boot project using IDEA and select the required dependencies.

4.1 Dependencies

The project mainly uses Redis, Thymeleaf, MyBatis‑Plus and other 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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3</version>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.4.1</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.72</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.22</version>
    </dependency>

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
    </dependency>

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.8.0</version>
    </dependency>

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.4.2.Final</version>
    </dependency>

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-jdk8</artifactId>
        <version>1.4.2.Final</version>
    </dependency>

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.4.2.Final</version>
    </dependency>

    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.10.6</version>
    </dependency>
</dependencies>

4.2 YML Configuration

After adding the dependencies, configure database connection, Redis, MyBatis‑Plus, thread pool, etc.

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
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      filters: stat,wall
  redis:
    port: 6379
    host: 127.0.0.1
    lettuce:
      pool:
        max-active: -1
        max-idle: 2000
        max-wait: -1
        min-idle: 1
        time-between-eviction-runs: 5000
  mvc:
    view:
      prefix: classpath:/templates/
      suffix: .html
# mybatis-plus
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
  mapper-locations: classpath*:mapper/**/*Mapper.xml
# thread pool
async:
  executor:
    thread:
      core-pool-size: 6
      max-pool-size: 12
      queue-capacity: 100000
      name-prefix: lottery-service-

4.3 Code Generation

Use MyBatis‑Plus code generator to create basic CRUD code, reducing repetitive work.

public class MybatisPlusGeneratorConfig {
    public static void main(String[] args) {
        // Code generator
        AutoGenerator mpg = new AutoGenerator();

        // Global config
        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 config
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        mpg.setDataSource(dsc);

        // Package config
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.example.lottery");
        pc.setEntity("dal.model");
        pc.setMapper("dal.mapper");
        pc.setService("service");
        pc.setServiceImpl("service.impl");
        mpg.setPackageInfo(pc);

        // Template config
        TemplateConfig templateConfig = new TemplateConfig();
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // Strategy config
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setSuperEntityClass("com.baomidou.mybatisplus.extension.activerecord.Model");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("lottery,lottery_item,lottery_prize,lottery_record").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }
}

4.4 Redis Configuration

If you use RedisTemplate in code, you need to add a configuration class to inject it into the Spring container.

@Configuration
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate
redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // Use Jackson2JsonRedisSerializer to replace the default serializer
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(DateTime.class, new JodaDateTimeJsonSerializer());
        simpleModule.addDeserializer(DateTime.class, new JodaDateTimeJsonDeserializer());
        objectMapper.registerModule(simpleModule);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        // Set value and key serializers
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

class JodaDateTimeJsonSerializer extends JsonSerializer
{
    @Override
    public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
    }
}

class JodaDateTimeJsonDeserializer extends JsonDeserializer
{
    @Override
    public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        String dateString = jsonParser.readValueAs(String.class);
        DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
        return dateTimeFormatter.parseDateTime(dateString);
    }
}

4.5 Constant Management

Common constants used across the code are extracted into a dedicated class.

public class LotteryConstants {

    /**
     * Flag for a user currently drawing
     */
    public final static String DRAWING = "DRAWING";

    /**
     * Activity flag: LOTTERY:lotteryID
     */
    public final static String LOTTERY = "LOTTERY";

    /**
     * Prize data: LOTTERY_PRIZE:lotteryID:PrizeId
     */
    public final static String LOTTERY_PRIZE = "LOTTERY_PRIZE";

    /**
     * Default prize data: DEFAULT_LOTTERY_PRIZE:lotteryID
     */
    public final static String DEFAULT_LOTTERY_PRIZE = "DEFAULT_LOTTERY_PRIZE";

    public enum PrizeTypeEnum {
        THANK(-1), NORMAL(1), UNIQUE(2);
        private int value;
        private PrizeTypeEnum(int value) { this.value = value; }
        public int getValue() { return this.value; }
    }

    /**
     * Prize cache: LOTTERY_ITEM:LOTTERY_ID
     */
    public final static String LOTTERY_ITEM = "LOTTERY_ITEM";

    /**
     * Default prize: DEFAULT_LOTTERY_ITEM:LOTTERY_ID
     */
    public final static String DEFAULT_LOTTERY_ITEM = "DEFAULT_LOTTERY_ITEM";
}
public enum ReturnCodeEnum {

    SUCCESS("0000", "成功"),
    LOTTER_NOT_EXIST("9001", "指定抽奖活动不存在"),
    LOTTER_FINISH("9002", "活动已结束"),
    LOTTER_REPO_NOT_ENOUGHT("9003", "当前奖品库存不足"),
    LOTTER_ITEM_NOT_INITIAL("9004", "奖项数据未初始化"),
    LOTTER_DRAWING("9005", "上一次抽奖还未结束"),
    REQUEST_PARAM_NOT_VALID("9998", "请求参数不正确"),
    SYSTEM_ERROR("9999", "系统繁忙,请稍后重试");

    private String code;
    private String msg;
    private ReturnCodeEnum(String code, String msg) { this.code = code; this.msg = msg; }
    public String getCode() { return code; }
    public String getMsg() { return msg; }
    public String getCodeString() { return getCode() + ""; }
}

Redis keys are managed uniformly through a helper class.

public class RedisKeyManager {

    public static String getDrawingRedisKey(String accountIp) {
        return new StringBuilder(LotteryConstants.DRAWING).append(":").append(accountIp).toString();
    }

    public static String getLotteryRedisKey(Integer id) {
        return new StringBuilder(LotteryConstants.LOTTERY).append(":").append(id).toString();
    }

    public static String getLotteryPrizeRedisKey(Integer lotteryId) {
        return new StringBuilder(LotteryConstants.LOTTERY_PRIZE).append(":").append(lotteryId).toString();
    }

    public static String getLotteryPrizeRedisKey(Integer lotteryId, Integer prizeId) {
        return new StringBuilder(LotteryConstants.LOTTERY_PRIZE).append(":").append(lotteryId).append(":").append(prizeId).toString();
    }

    public static String getDefaultLotteryPrizeRedisKey(Integer lotteryId) {
        return new StringBuilder(LotteryConstants.DEFAULT_LOTTERY_PRIZE).append(":").append(lotteryId).toString();
    }

    public static String getLotteryItemRedisKey(Integer lotteryId) {
        return new StringBuilder(LotteryConstants.LOTTERY_ITEM).append(":").append(lotteryId).toString();
    }

    public static String getDefaultLotteryItemRedisKey(Integer lotteryId) {
        return new StringBuilder(LotteryConstants.DEFAULT_LOTTERY_ITEM).append(":").append(lotteryId).toString();
    }
}

4.6 Business Code

4.6.1 Lottery API

The controller receives the lottery ID from the front‑end, validates the request, performs the draw, and returns the result.

@GetMapping("/{id}")
public ResultResp
doDraw(@PathVariable("id") Integer id, HttpServletRequest request) {
    String accountIp = CusAccessObjectUtil.getIpAddress(request);
    log.info("begin LotteryController.doDraw, access user {}, lotteryId,{}:", accountIp, id);
    ResultResp
resultResp = new ResultResp<>();
    try {
        // Check whether the previous draw has finished
        checkDrawParams(id, accountIp);
        // Perform draw
        DoDrawDto dto = new DoDrawDto();
        dto.setAccountIp(accountIp);
        dto.setLotteryId(id);
        lotteryService.doDraw(dto);
        // Set response
        resultResp.setCode(ReturnCodeEnum.SUCCESS.getCode());
        resultResp.setMsg(ReturnCodeEnum.SUCCESS.getMsg());
        resultResp.setResult(lotteryConverter.dto2LotteryItemVo(dto));
    } catch (Exception e) {
        return ExceptionUtil.handlerException4biz(resultResp, e);
    } finally {
        // Clear the drawing flag
        redisTemplate.delete(RedisKeyManager.getDrawingRedisKey(accountIp));
    }
    return resultResp;
}

private void checkDrawParams(Integer id, String accountIp) {
    if (null == id) {
        throw new RewardException(ReturnCodeEnum.REQUEST_PARAM_NOT_VALID.getCode(), ReturnCodeEnum.REQUEST_PARAM_NOT_VALID.getMsg());
    }
    // Use setNx to ensure the previous draw has finished
    Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getDrawingRedisKey(accountIp), "1", 60, TimeUnit.SECONDS);
    if (!result) {
        throw new RewardException(ReturnCodeEnum.LOTTER_DRAWING.getCode(), ReturnCodeEnum.LOTTER_DRAWING.getMsg());
    }
}

Redis setNx prevents duplicate draw requests from the same user.

4.6.2 Data Initialization

After the request passes validation, the service loads prize data into Redis using an event‑driven approach.

@Override
public void doDraw(DoDrawDto drawDto) throws Exception {
    RewardContext context = new RewardContext();
    LotteryItem lotteryItem = null;
    try {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        // Validate activity
        Lottery lottery = checkLottery(drawDto);
        // Publish event to load prize data into Redis
        applicationContext.publishEvent(new InitPrizeToRedisEvent(this, lottery.getId(), countDownLatch));
        // Perform draw
        lotteryItem = doPlay(lottery);
        // Wait for prize data initialization
        countDownLatch.await();
        String key = RedisKeyManager.getLotteryPrizeRedisKey(lottery.getId(), lotteryItem.getPrizeId());
        int prizeType = Integer.parseInt(redisTemplate.opsForHash().get(key, "prizeType").toString());
        context.setLottery(lottery);
        context.setLotteryItem(lotteryItem);
        context.setAccountIp(drawDto.getAccountIp());
        context.setKey(key);
        // Adjust stock and record winning info
        AbstractRewardProcessor.rewardProcessorMap.get(prizeType).doReward(context);
    } catch (UnRewardException u) {
        // No prize, return default item
        context.setKey(RedisKeyManager.getDefaultLotteryPrizeRedisKey(lotteryItem.getLotteryId()));
        lotteryItem = (LotteryItem) redisTemplate.opsForValue().get(RedisKeyManager.getDefaultLotteryItemRedisKey(lotteryItem.getLotteryId()));
        context.setLotteryItem(lotteryItem);
        AbstractRewardProcessor.rewardProcessorMap.get(LotteryConstants.PrizeTypeEnum.THANK.getValue()).doReward(context);
    }
    // Populate response data
    drawDto.setLevel(lotteryItem.getLevel());
    drawDto.setPrizeName(context.getPrizeName());
    drawDto.setPrizeId(context.getPrizeId());
}

The event InitPrizeToRedisEvent carries the lottery ID and a CountDownLatch to synchronize the initialization.

public class InitPrizeToRedisEvent extends ApplicationEvent {

    private Integer lotteryId;
    private CountDownLatch countDownLatch;

    public InitPrizeToRedisEvent(Object source, Integer lotteryId, CountDownLatch countDownLatch) {
        super(source);
        this.lotteryId = lotteryId;
        this.countDownLatch = countDownLatch;
    }
    public Integer getLotteryId() { return lotteryId; }
    public void setLotteryId(Integer lotteryId) { this.lotteryId = lotteryId; }
    public CountDownLatch getCountDownLatch() { return countDownLatch; }
    public void setCountDownLatch(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; }
}

The listener loads activity, prize and item data from the database into Redis, using HASH structures for atomic stock operations.

@Slf4j
@Component
public class InitPrizeToRedisListener implements ApplicationListener
{

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    LotteryPrizeMapper lotteryPrizeMapper;

    @Autowired
    LotteryItemMapper lotteryItemMapper;

    @Override
    public void onApplicationEvent(InitPrizeToRedisEvent event) {
        log.info("begin InitPrizeToRedisListener," + event);
        Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getLotteryPrizeRedisKey(event.getLotteryId()), "1");
        if (!result) {
            log.info("already initial");
            event.getCountDownLatch().countDown();
            return;
        }
        // Load items
        QueryWrapper
itemWrapper = new QueryWrapper<>();
        itemWrapper.eq("lottery_id", event.getLotteryId());
        List
items = lotteryItemMapper.selectList(itemWrapper);
        LotteryItem defaultItem = items.parallelStream().filter(o -> o.getDefaultItem().intValue() == 1).findFirst().orElse(null);
        Map
itemMap = new HashMap<>(16);
        itemMap.put(RedisKeyManager.getLotteryItemRedisKey(event.getLotteryId()), items);
        itemMap.put(RedisKeyManager.getDefaultLotteryItemRedisKey(event.getLotteryId()), defaultItem);
        redisTemplate.opsForValue().multiSet(itemMap);
        // Load prizes
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("lottery_id", event.getLotteryId());
        List
prizes = lotteryPrizeMapper.selectList(queryWrapper);
        AtomicReference
defaultPrize = new AtomicReference<>();
        prizes.forEach(prize -> {
            if (prize.getId().equals(defaultItem.getPrizeId())) {
                defaultPrize.set(prize);
            }
            String key = RedisKeyManager.getLotteryPrizeRedisKey(event.getLotteryId(), prize.getId());
            setLotteryPrizeToRedis(key, prize);
        });
        String defaultKey = RedisKeyManager.getDefaultLotteryPrizeRedisKey(event.getLotteryId());
        setLotteryPrizeToRedis(defaultKey, defaultPrize.get());
        event.getCountDownLatch().countDown();
        log.info("finish InitPrizeToRedisListener," + event);
    }

    private void setLotteryPrizeToRedis(String key, LotteryPrize prize) {
        redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        redisTemplate.opsForHash().put(key, "id", prize.getId());
        redisTemplate.opsForHash().put(key, "lotteryId", prize.getLotteryId());
        redisTemplate.opsForHash().put(key, "prizeName", prize.getPrizeName());
        redisTemplate.opsForHash().put(key, "prizeType", prize.getPrizeType());
        redisTemplate.opsForHash().put(key, "totalStock", prize.getTotalStock());
        redisTemplate.opsForHash().put(key, "validStock", prize.getValidStock());
    }
}

Alternatively, an ApplicationRunner can load data at application startup.

@Slf4j
@Component
public class LoadDataApplicationRunner implements ApplicationRunner {

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    LotteryMapper lotteryMapper;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("=========begin load lottery data to Redis===========");
        Lottery lottery = lotteryMapper.selectById(1);
        // Additional loading logic can be added here
        log.info("=========finish load lottery data to Redis===========");
    }
}

4.6.3 Lottery Logic

The draw process first attempts to fetch prize data from Redis; if missing, it falls back to the database and caches the result. It then shuffles the prize list, builds probability intervals, generates a random number, and selects the winning prize.

private LotteryItem doPlay(Lottery lottery) {
    LotteryItem lotteryItem = null;
    QueryWrapper
queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("lottery_id", lottery.getId());
    Object lotteryItemsObj = redisTemplate.opsForValue().get(RedisKeyManager.getLotteryItemRedisKey(lottery.getId()));
    List
lotteryItems;
    if (lotteryItemsObj == null) {
        lotteryItems = lotteryItemMapper.selectList(queryWrapper);
    } else {
        lotteryItems = (List
) lotteryItemsObj;
    }
    if (lotteryItems.isEmpty()) {
        throw new BizException(ReturnCodeEnum.LOTTER_ITEM_NOT_INITIAL.getCode(), ReturnCodeEnum.LOTTER_ITEM_NOT_INITIAL.getMsg());
    }
    int lastScope = 0;
    Collections.shuffle(lotteryItems);
    Map
awardItemScope = new HashMap<>();
    for (LotteryItem item : lotteryItems) {
        int currentScope = lastScope + new BigDecimal(item.getPercent().floatValue()).multiply(new BigDecimal(mulriple)).intValue();
        awardItemScope.put(item.getId(), new int[]{lastScope + 1, currentScope});
        lastScope = currentScope;
    }
    int luckyNumber = new Random().nextInt(mulriple);
    int luckyPrizeId = 0;
    if (!awardItemScope.isEmpty()) {
        for (Map.Entry
entry : awardItemScope.entrySet()) {
            if (luckyNumber >= entry.getValue()[0] && luckyNumber <= entry.getValue()[1]) {
                luckyPrizeId = entry.getKey();
                break;
            }
        }
    }
    for (LotteryItem item : lotteryItems) {
        if (item.getId().intValue() == luckyPrizeId) {
            lotteryItem = item;
            break;
        }
    }
    return lotteryItem;
}

4.6.4 Stock Adjustment and Recording

Different prize types are handled by distinct processors. For normal prizes, the stock is decremented atomically in Redis; if the stock becomes negative, an exception triggers a fallback to a default prize.

public interface RewardProcessor
{
    void doReward(RewardContext context);
}
public abstract class AbstractRewardProcessor implements RewardProcessor
, ApplicationContextAware {

    public static Map
rewardProcessorMap = new ConcurrentHashMap<>();

    @Autowired
    protected RedisTemplate redisTemplate;

    @Override
    public void doReward(RewardContext context) {
        beforeProcessor(context);
        processor(context);
        afterProcessor(context);
    }

    protected abstract void beforeProcessor(RewardContext context);
    protected abstract void processor(RewardContext context);
    protected abstract void afterProcessor(RewardContext context);
    protected abstract int getAwardType();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        rewardProcessorMap.put(LotteryConstants.PrizeTypeEnum.THANK.getValue(), (RewardProcessor) applicationContext.getBean(NoneStockRewardProcessor.class));
        rewardProcessorMap.put(LotteryConstants.PrizeTypeEnum.NORMAL.getValue(), (RewardProcessor) applicationContext.getBean(HasStockRewardProcessor.class));
    }
}

Normal‑stock processor example:

@Override
protected void processor(RewardContext context) {
    // Decrement stock atomically in Redis
    Long result = redisTemplate.opsForHash().increment(context.getKey(), "validStock", -1);
    if (result.intValue() < 0) {
        throw new UnRewardException(ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getCode(), ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getMsg());
    }
    List
properties = Arrays.asList("id", "prizeName");
    List
prizes = redisTemplate.opsForHash().multiGet(context.getKey(), properties);
    context.setPrizeId(Integer.parseInt(prizes.get(0).toString()));
    context.setPrizeName(prizes.get(1).toString());
    // Update DB stock
    lotteryPrizeMapper.updateValidStock(context.getPrizeId());
}

After processing, an asynchronous task records the winning information.

@Override
protected void afterProcessor(RewardContext context) {
    asyncLotteryRecordTask.saveLotteryRecord(context.getAccountIp(), context.getLotteryItem(), context.getPrizeName());
}
@Slf4j
@Component
public class AsyncLotteryRecordTask {

    @Autowired
    LotteryRecordMapper lotteryRecordMapper;

    @Async("lotteryServiceExecutor")
    public void saveLotteryRecord(String accountIp, LotteryItem lotteryItem, String prizeName) {
        log.info(Thread.currentThread().getName() + "---saveLotteryRecord");
        LotteryRecord record = new LotteryRecord();
        record.setAccountIp(accountIp);
        record.setItemId(lotteryItem.getId());
        record.setPrizeName(prizeName);
        record.setCreateTime(LocalDateTime.now());
        lotteryRecordMapper.insert(record);
    }
}

Thread‑pool configuration defined in YML and wired via a @Bean.

@Configuration
@EnableAsync
@EnableConfigurationProperties(ThreadPoolExecutorProperties.class)
public class ThreadPoolExecutorConfig {

    @Bean(name = "lotteryServiceExecutor")
    public Executor lotteryServiceExecutor(ThreadPoolExecutorProperties poolExecutorProperties) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(poolExecutorProperties.getCorePoolSize());
        executor.setMaxPoolSize(poolExecutorProperties.getMaxPoolSize());
        executor.setQueueCapacity(poolExecutorProperties.getQueueCapacity());
        executor.setThreadNamePrefix(poolExecutorProperties.getNamePrefix());
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}
@Data
@ConfigurationProperties(prefix = "async.executor.thread")
public class ThreadPoolExecutorProperties {
    private int corePoolSize;
    private int maxPoolSize;
    private int queueCapacity;
    private String namePrefix;
}

4.7 Summary

The article walks through the complete setup of a Spring Boot lottery project, from project creation, dependency configuration, database schema, Redis caching, event‑driven data initialization, draw algorithm, stock handling, to asynchronous record persistence. The front‑end simply calls the back‑end API and displays the prize returned.

5. Project Repository

https://gitee.com/cl1429745331/redis-demo

If you use the project directly, remember to adjust the activity end time in the database. The core implementation resides in the lottery module.

javabackend developmentRedisSpring BootMyBatis-Pluslottery
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.