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.
The specific practical project is in lottery project.