Redis project practice - lottery turntable

1. Project introduction

This is a simple case based on Spring boot + Mybatis Plus + Redis.

It mainly caches the activity content, prize information, record information, etc. into Redis, and then all lottery processes do data operations from Redis.

The general content is very simple, and the specific operation is analyzed slowly below.

2. Project demonstration

Without much to say, first look at the project effect in the figure above. If it's OK, let's see how it is realized.

3. Table structure

The project includes the following four tables: activity table, award table, prize table and winning record table. The specific SQL will be given at the end of the article.

4. Project construction

Let's first build a standard Spring boot project, directly create it in the IDEA, and then select some related dependencies.

4.1 dependency

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 dependency is introduced, we need to configure the database connection information, 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

Here, we can directly use the code generator of mybatis plus to help us generate some basic business code and avoid these repetitive manual tasks.

Relevant codes are posted here. You can directly modify the database connection information, relevant package name and module name.

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

        // Global configuration
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("chen");
        gc.setOpen(false);
        //Entity attribute Swagger2 annotation
        gc.setSwagger2(false);
        mpg.setGlobalConfig(gc);

        // Data source configuration
        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 configuration
        PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("module name");
        pc.setParent("com.example.lottery");
        pc.setEntity("dal.model");
        pc.setMapper("dal.mapper");
        pc.setService("service");
        pc.setServiceImpl("service.impl");
        mpg.setPackageInfo(pc);


        // Configuration template
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // Policy configuration
        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.setEntityLombokModel(true);
        // Public parent class
//        strategy.setSuperControllerClass("com.baomidou.ant.common.BaseController");
        // Public fields written in the parent class
//        strategy.setSuperEntityColumns("id");
        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("Please enter" + tip + ": ");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("Please enter the correct" + tip + "!");
    }
}

4.4 Redis configuration

If we use RedisTemplate in our code, we need to add relevant configurations and inject them into the Spring container.

@Configuration
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // Replace the default serialization with Jackson2JsonRedisSerialize
        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 the serialization rules of value and key
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> {
    @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<DateTime> {
    @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

Since some common constants are used in the code, we should isolate them.

public class LotteryConstants {

    /**
     * Indicates the user tag that is drawing the lottery
     */
    public final static String DRAWING = "DRAWING";
    
    /**
     * Activity tag: 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;
        }
    }

    /**
     * Award cache: LOTTERY_ITEM:LOTTERY_ID
     */
    public final static String LOTTERY_ITEM = "LOTTERY_ITEM";
    
    /**
     * Default Award: DEFAULT_LOTTERY_ITEM:LOTTERY_ID
     */
    public final static String DEFAULT_LOTTERY_ITEM = "DEFAULT_LOTTERY_ITEM";

}
public enum ReturnCodeEnum {

    SUCCESS("0000", "success"),

    LOTTER_NOT_EXIST("9001", "The specified raffle does not exist"),

    LOTTER_FINISH("9002", "Activity ended"),

    LOTTER_REPO_NOT_ENOUGHT("9003", "The current prize inventory is insufficient"),

    LOTTER_ITEM_NOT_INITIAL("9004", "Award data not initialized"),

    LOTTER_DRAWING("9005", "The last raffle is not over yet"),

    REQUEST_PARAM_NOT_VALID("9998", "The request parameters are incorrect"),

    SYSTEM_ERROR("9999", "System busy,Please try again later");

    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() + "";
    }
}

Unified management of key s in Redis.

public class RedisKeyManager {

    /**
     * key drawing
     *
     * @param accountIp
     * @return
     */
    public static String getDrawingRedisKey(String accountIp) {
        return new StringBuilder(LotteryConstants.DRAWING).append(":").append(accountIp).toString();
    }

    /**
     * Get the key of the lucky draw
     *
     * @param id
     * @return
     */
    public static String getLotteryRedisKey(Integer id) {
        return new StringBuilder(LotteryConstants.LOTTERY).append(":").append(id).toString();
    }

    /**
     * Get all prize data under the specified activity
     *
     * @param lotteryId
     * @return
     */
    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 interface

We first write the lottery interface, query the specific activities according to the parameters passed by the foreground, and then carry out the corresponding operations. (of course, the front end is written dead directly / lottery/1)

@GetMapping("/{id}")
public ResultResp<LotteryItemVo> doDraw(@PathVariable("id") Integer id, HttpServletRequest request) {
    String accountIp = CusAccessObjectUtil.getIpAddress(request);
    log.info("begin LotteryController.doDraw,access user {}, lotteryId,{}:", accountIp, id);
    ResultResp<LotteryItemVo> resultResp = new ResultResp<>();
    try {
        //Judge whether the last lucky draw of the current user is over
        checkDrawParams(id, accountIp);

        //luck draw
        DoDrawDto dto = new DoDrawDto();
        dto.setAccountIp(accountIp);
        dto.setLotteryId(id);
        lotteryService.doDraw(dto);

        //Return result settings
        resultResp.setCode(ReturnCodeEnum.SUCCESS.getCode());
        resultResp.setMsg(ReturnCodeEnum.SUCCESS.getMsg());
        //Object conversion
        resultResp.setResult(lotteryConverter.dto2LotteryItemVo(dto));
    } catch (Exception e) {
        return ExceptionUtil.handlerException4biz(resultResp, e);
    } finally {
        //Clear placeholder
        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 the setNx command to judge whether the last lucky draw of the current user is over
    Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getDrawingRedisKey(accountIp), "1", 60, TimeUnit.SECONDS);
    //If it is false, it means that the last lucky draw has not ended
    if (!result) {
        throw new RewardException(ReturnCodeEnum.LOTTER_DRAWING.getCode(), ReturnCodeEnum.LOTTER_DRAWING.getMsg());
    }
}

In order to avoid users clicking on the lottery repeatedly, we use Redis to avoid this problem. Each time a user draws a lottery, they queue up for the user through setNx and set the expiration time; If a user clicks the lottery several times and Redis finds that the user's last lottery has not ended when setting the value, an exception will be thrown.

Finally, if the user wins the lottery, remember to clear the mark so that the user can continue the lottery.

4.6. 2 initialization data

Enter from the lottery entrance and start the business operation after the verification is successful.

@Override
public void doDraw(DoDrawDto drawDto) throws Exception {
    RewardContext context = new RewardContext();
    LotteryItem lotteryItem = null;
    try {
        //The JUC tool needs to wait for the thread to end before it can run
        CountDownLatch countDownLatch = new CountDownLatch(1);
        //Judge activity effectiveness
        Lottery lottery = checkLottery(drawDto);
        //Publish event, which is used to load the prize information of the specified activity
        applicationContext.publishEvent(new InitPrizeToRedisEvent(this, lottery.getId(), countDownLatch));
        //Start the lottery
        lotteryItem = doPlay(lottery);
        //Record prizes and deduct inventory
        countDownLatch.await(); //Wait for the prize initialization to complete
        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 inventory and record winning information
        AbstractRewardProcessor.rewardProcessorMap.get(prizeType).doReward(context);
    } catch (UnRewardException u) { //Indicates that a default award is returned because some problems do not win the prize
        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);
    }
    //Splice return data
    drawDto.setLevel(lotteryItem.getLevel());
    drawDto.setPrizeName(context.getPrizeName());
    drawDto.setPrizeId(context.getPrizeId());
}

First, we use CountDownLatch to ensure the order of commodity initialization. You can see about CountDownLatch JUC tools The article.

Then we need to check the effectiveness of the activity to ensure that the activity is not over.

After the inspection activity passes, the prize data is loaded through the ApplicationEvent event and stored in Redis. Or get relevant data when the program starts through ApplicationRunner. We use the event mechanism. The relevant code of ApplicationRunner is also posted below.

Event mechanism

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;
    }

}

With the event mechanism, we also need a listening event to initialize relevant data information. You can refer to the following code for specific business logic. There are relevant annotation information, which is mainly to add the data in the database to redis. It should be noted that in order to ensure the atomicity, we store the data through HASH, so that the atomicity can be guaranteed through opsForHash during inventory deduction.

After initializing the prize information, the execution of the table name is completed through the countDown() method, and the execution can continue where the thread is blocked in the business code.

@Slf4j
@Component
public class InitPrizeToRedisListener implements ApplicationListener<InitPrizeToRedisEvent> {

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    LotteryPrizeMapper lotteryPrizeMapper;

    @Autowired
    LotteryItemMapper lotteryItemMapper;

    @Override
    public void onApplicationEvent(InitPrizeToRedisEvent initPrizeToRedisEvent) {
        log.info("begin InitPrizeToRedisListener," + initPrizeToRedisEvent);
        Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId()), "1");
        //It has been initialized into the cache. You don't need to cache again
        if (!result) {
            log.info("already initial");
            initPrizeToRedisEvent.getCountDownLatch().countDown();
            return;
        }
        QueryWrapper<LotteryItem> lotteryItemQueryWrapper = new QueryWrapper<>();
        lotteryItemQueryWrapper.eq("lottery_id", initPrizeToRedisEvent.getLotteryId());
        List<LotteryItem> lotteryItems = lotteryItemMapper.selectList(lotteryItemQueryWrapper);

        //If the specified prize is not available, a default prize will be generated
        LotteryItem defaultLotteryItem = lotteryItems.parallelStream().filter(o -> o.getDefaultItem().intValue() == 1).findFirst().orElse(null);

        Map<String, Object> lotteryItemMap = new HashMap<>(16);
        lotteryItemMap.put(RedisKeyManager.getLotteryItemRedisKey(initPrizeToRedisEvent.getLotteryId()), lotteryItems);
        lotteryItemMap.put(RedisKeyManager.getDefaultLotteryItemRedisKey(initPrizeToRedisEvent.getLotteryId()), defaultLotteryItem);
        redisTemplate.opsForValue().multiSet(lotteryItemMap);

        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("lottery_id", initPrizeToRedisEvent.getLotteryId());
        List<LotteryPrize> lotteryPrizes = lotteryPrizeMapper.selectList(queryWrapper);

        //Save a default Award
        AtomicReference<LotteryPrize> defaultPrize = new AtomicReference<>();
        lotteryPrizes.stream().forEach(lotteryPrize -> {
            if (lotteryPrize.getId().equals(defaultLotteryItem.getPrizeId())) {
                defaultPrize.set(lotteryPrize);
            }
            String key = RedisKeyManager.getLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId(), lotteryPrize.getId());
            setLotteryPrizeToRedis(key, lotteryPrize);
        });
        String key = RedisKeyManager.getDefaultLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId());
        setLotteryPrizeToRedis(key, defaultPrize.get());
        initPrizeToRedisEvent.getCountDownLatch().countDown(); //Indicates that initialization is complete
        log.info("finish InitPrizeToRedisListener," + initPrizeToRedisEvent);
    }

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

The above part initializes data through the event method. Let's talk about the method of ApplicationRunner:

This method is very simple. You can load the data when the project starts.

We only need to implement the ApplicationRunner interface, and then read data from the database in the run method and load it into Redis.

@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===========");
        //Load current raffle activity information
        Lottery lottery = lotteryMapper.selectById(1);

        log.info("=========finish load lottery data to Redis===========");
    }
}

4.6. 3 lucky draw

When we use events for data initialization, we can draw at the same time, but note that countdownlatch. Com needs to be used at this time await(); To block the current thread and wait for data initialization to complete.

During the lottery, we first try to obtain relevant data from Redis. If there is no data in Redis, we load the data from the database. If there is no relevant data queried in the database, it indicates that the relevant data has not been configured.

After we get the data, we should start the lottery. The core of the lottery is randomness and probability. We can't draw the first prize at random, can we? So we need to set the probability of each award in the table. As follows:

When we draw, we need to divide relevant intervals according to probability. We can check how to divide it by debugging:

The greater the probability of awards, the greater the interval; The order you see is different, because we use collections shuffle(lotteryItems); It disrupts the collection, so what you see here is not displayed in order.

After generating the corresponding interval, we generate random numbers to see if the random numbers fall in that interval, and then return the corresponding awards. This realizes our lottery process.

private LotteryItem doPlay(Lottery lottery) {
    LotteryItem lotteryItem = null;
    QueryWrapper<LotteryItem> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("lottery_id", lottery.getId());
    Object lotteryItemsObj = redisTemplate.opsForValue().get(RedisKeyManager.getLotteryItemRedisKey(lottery.getId()));
    List<LotteryItem> lotteryItems;
    //The description has not been loaded into the cache. It is loaded synchronously from the database and the data is cached asynchronously
    if (lotteryItemsObj == null) {
        lotteryItems = lotteryItemMapper.selectList(queryWrapper);
    } else {
        lotteryItems = (List<LotteryItem>) lotteryItemsObj;
    }
    //Award data not configured
    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<Integer, int[]> awardItemScope = new HashMap<>();
    //item.getPercent=0.05 = 5%
    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()) {
        Set<Map.Entry<Integer, int[]>> set = awardItemScope.entrySet();
        for (Map.Entry<Integer, int[]> entry : set) {
            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 adjust inventory and record

When adjusting the inventory, we need to take into account the different types of prizes and take different measures according to different types of prizes. For example, if there are some high-value prizes, we need to ensure security through distributed locks; Or, for example, we need to send corresponding SMS for some goods; Therefore, we need to adopt an extensible implementation mechanism.

The specific implementation mechanism can be seen in the class diagram below. I first define a reward method interface (RewardProcessor), and then define an abstract class (AbstractRewardProcessor). The template method is defined in the abstract class, and then we can create different processors according to different types, which greatly enhances our scalability.

For example, we have created processors with sufficient inventory and processors with insufficient inventory.

Interface:

public interface RewardProcessor<T> {

    void doReward(RewardContext context);

}

Abstract class:

@Slf4j
public abstract class AbstractRewardProcessor implements RewardProcessor<RewardContext>, ApplicationContextAware {

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

    @Autowired
    protected RedisTemplate redisTemplate;

    private void beforeProcessor(RewardContext context) {
    }

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

    protected abstract void afterProcessor(RewardContext context);


    /**
     * Distribute corresponding prizes
     *
     * @param context
     */
    protected abstract void processor(RewardContext context);

    /**
     * Returns the current prize type
     *
     * @return
     */
    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));
    }
}

We can start with the doReward method in the abstract class. For example, let's first look at the code in the processor with sufficient inventory:

When the inventory processor executes, the prime minister will reduce the corresponding award inventory in Redis by 1. At this time, locking is not necessary because this operation is atomic.

After deduction, we judge whether the commodity inventory is sufficient according to the returned value. At this time, if the inventory is insufficient, we will prompt that we have not won the prize or return a default commodity.

Finally, we need to remember to update the relevant data in the database.

@Override
protected void processor(RewardContext context) {
    //Inventory deduction (update of redis)
    Long result = redisTemplate.opsForHash().increment(context.getKey(), "validStock", -1);
    //The current prize inventory is insufficient. It indicates that you have not won the prize, or return a prize with the bottom
    if (result.intValue() < 0) {
        throw new UnRewardException(ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getCode(), ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getMsg());
    }
    List<Object> propertys = Arrays.asList("id", "prizeName");
    List<Object> prizes = redisTemplate.opsForHash().multiGet(context.getKey(), propertys);
    context.setPrizeId(Integer.parseInt(prizes.get(0).toString()));
    context.setPrizeName(prizes.get(1).toString());
    //Update inventory (update of database)
    lotteryPrizeMapper.updateValidStock(context.getPrizeId());
}

After the method is executed, we need to execute the afterProcessor method:

In this place, we store lottery record information asynchronously through asynchronous tasks.

@Override
protected void afterProcessor(RewardContext context) {
    asyncLotteryRecordTask.saveLotteryRecord(context.getAccountIp(), context.getLotteryItem(), context.getPrizeName());
}

Here, we can find that Async annotation is used to specify a thread pool and start an asynchronous execution method.

@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");
        //Store winning information
        LotteryRecord record = new LotteryRecord();
        record.setAccountIp(accountIp);
        record.setItemId(lotteryItem.getId());
        record.setPrizeName(prizeName);
        record.setCreateTime(LocalDateTime.now());
        lotteryRecordMapper.insert(record);
    }
}

Create a thread pool: the relevant configuration information is the data defined in the YML file.

@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 above is the construction of the whole project. The front-end interface is nothing more than sending a request to the back end. According to the returned prize information, drop the pointer at the corresponding turntable position. The specific code can be viewed at the project address. I hope you can move a little hand and praise, hee hee.

5. Project address

If you use the project directly, remember to modify the end time of the activity in the database.

Redis

The specific practical project is in lottery project.

Keywords: Redis Spring Boot Cache

Added by gewthen on Sat, 01 Jan 2022 23:32:16 +0200