Redis?抽獎大轉(zhuǎn)盤的實(shí)戰(zhàn)示例
1. 項(xiàng)目介紹
這是一個(gè)基于Spring boot + Mybatis Plus + Redis 的簡單案例。
主要是將活動內(nèi)容、獎品信息、記錄信息等緩存到Redis中,然后所有的抽獎過程全部從Redis中做數(shù)據(jù)的操作。
大致內(nèi)容很簡單,具體操作下面慢慢分析。
2. 項(xiàng)目演示
話不多說,首先上圖看看項(xiàng)目效果,如果覺得還行的話咱們就來看看他具體是怎么實(shí)現(xiàn)的。
3. 表結(jié)構(gòu)
該項(xiàng)目包含以下四張表,分別是活動表、獎項(xiàng)表、獎品表以及中獎記錄表。具體的SQL會在文末給出。
4. 項(xiàng)目搭建
咱們首先先搭建一個(gè)標(biāo)準(zhǔn)的Spring boot 項(xiàng)目,直接IDEA創(chuàng)建,然后選擇一些相關(guān)的依賴即可。
4.1 依賴
該項(xiàng)目主要用到了:Redis,thymeleaf,mybatis-plus等依賴。
<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配置
依賴引入之后,我們需要進(jìn)行相應(yīng)的配置:數(shù)據(jù)庫連接信息、Redis、mybatis-plus、線程池等。
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 # 線程池 async: executor: thread: core-pool-size: 6 max-pool-size: 12 queue-capacity: 100000 name-prefix: lottery-service-
4.3 代碼生成
這邊我們可以直接使用mybatis-plus的代碼生成器幫助我們生成一些基礎(chǔ)的業(yè)務(wù)代碼,避免這些重復(fù)的體力活。
這邊貼出相關(guān)代碼,直接修改數(shù)據(jù)庫連接信息、相關(guān)包名模塊名即可。
public class MybatisPlusGeneratorConfig { public static void main(String[] args) { // 代碼生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("chen"); gc.setOpen(false); //實(shí)體屬性 Swagger2 注解 gc.setSwagger2(false); mpg.setGlobalConfig(gc); // 數(shù)據(jù)源配置 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); // 包配置 PackageConfig pc = new PackageConfig(); // pc.setModuleName(scanner("模塊名")); pc.setParent("com.example.lottery"); pc.setEntity("dal.model"); pc.setMapper("dal.mapper"); pc.setService("service"); pc.setServiceImpl("service.impl"); mpg.setPackageInfo(pc); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 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); // 公共父類 // strategy.setSuperControllerClass("com.baomidou.ant.common.BaseController"); // 寫于父類中的公共字段 // 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("請輸入" + 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 配置
我們?nèi)绻诖a中使用 RedisTemplate 的話,需要添加相關(guān)配置,將其注入到Spring容器中。
@Configuration public class RedisTemplateConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 使用Jackson2JsonRedisSerialize 替換默認(rèn)序列化 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); // 設(shè)置value的序列化規(guī)則和 key的序列化規(guī)則 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 常量管理
由于代碼中會用到一些共有的常量,我們應(yīng)該將其抽離出來。
public class LotteryConstants { /** * 表示正在抽獎的用戶標(biāo)記 */ public final static String DRAWING = "DRAWING"; /** * 活動標(biāo)記 LOTTERY:lotteryID */ public final static String LOTTERY = "LOTTERY"; /** * 獎品數(shù)據(jù) LOTTERY_PRIZE:lotteryID:PrizeId */ public final static String LOTTERY_PRIZE = "LOTTERY_PRIZE"; /** * 默認(rèn)獎品數(shù)據(jù) 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; } } /** * 獎項(xiàng)緩存:LOTTERY_ITEM:LOTTERY_ID */ public final static String LOTTERY_ITEM = "LOTTERY_ITEM"; /** * 默認(rèn)獎項(xiàng): 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", "活動已結(jié)束"), LOTTER_REPO_NOT_ENOUGHT("9003", "當(dāng)前獎品庫存不足"), LOTTER_ITEM_NOT_INITIAL("9004", "獎項(xiàng)數(shù)據(jù)未初始化"), LOTTER_DRAWING("9005", "上一次抽獎還未結(jié)束"), REQUEST_PARAM_NOT_VALID("9998", "請求參數(shù)不正確"), SYSTEM_ERROR("9999", "系統(tǒng)繁忙,請稍后重試"); 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中的key進(jìn)行統(tǒng)一的管理。
public class RedisKeyManager { /** * 正在抽獎的key * * @param accountIp * @return */ public static String getDrawingRedisKey(String accountIp) { return new StringBuilder(LotteryConstants.DRAWING).append(":").append(accountIp).toString(); } /** * 獲取抽獎活動的key * * @param id * @return */ public static String getLotteryRedisKey(Integer id) { return new StringBuilder(LotteryConstants.LOTTERY).append(":").append(id).toString(); } /** * 獲取指定活動下的所有獎品數(shù)據(jù) * * @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 業(yè)務(wù)代碼
4.6.1 抽獎接口
我們首先編寫抽獎接口,根據(jù)前臺傳的參數(shù)查詢到具體的活動,然后進(jìn)行相應(yīng)的操作。(當(dāng)然,前端直接是寫死的/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 { //判斷當(dāng)前用戶上一次抽獎是否結(jié)束 checkDrawParams(id, accountIp); //抽獎 DoDrawDto dto = new DoDrawDto(); dto.setAccountIp(accountIp); dto.setLotteryId(id); lotteryService.doDraw(dto); //返回結(jié)果設(shè)置 resultResp.setCode(ReturnCodeEnum.SUCCESS.getCode()); resultResp.setMsg(ReturnCodeEnum.SUCCESS.getMsg()); //對象轉(zhuǎn)換 resultResp.setResult(lotteryConverter.dto2LotteryItemVo(dto)); } catch (Exception e) { return ExceptionUtil.handlerException4biz(resultResp, e); } finally { //清除占位標(biāo)記 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()); } //采用setNx命令,判斷當(dāng)前用戶上一次抽獎是否結(jié)束 Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getDrawingRedisKey(accountIp), "1", 60, TimeUnit.SECONDS); //如果為false,說明上一次抽獎還未結(jié)束 if (!result) { throw new RewardException(ReturnCodeEnum.LOTTER_DRAWING.getCode(), ReturnCodeEnum.LOTTER_DRAWING.getMsg()); } }
為了避免用戶重復(fù)點(diǎn)擊抽獎,所以我們通過Redis來避免這種問題,用戶每次抽獎的時(shí)候,通過setNx給用戶排隊(duì)并設(shè)置過期時(shí)間;如果用戶點(diǎn)擊多次抽獎,Redis設(shè)置值的時(shí)候發(fā)現(xiàn)該用戶上次抽獎還未結(jié)束則拋出異常。
最后用戶抽獎成功的話,記得清除該標(biāo)記,從而用戶能夠繼續(xù)抽獎。
4.6.2 初始化數(shù)據(jù)
從抽獎入口進(jìn)來,校驗(yàn)成功以后則開始業(yè)務(wù)操作。
@Override public void doDraw(DoDrawDto drawDto) throws Exception { RewardContext context = new RewardContext(); LotteryItem lotteryItem = null; try { //JUC工具 需要等待線程結(jié)束之后才能運(yùn)行 CountDownLatch countDownLatch = new CountDownLatch(1); //判斷活動有效性 Lottery lottery = checkLottery(drawDto); //發(fā)布事件,用來加載指定活動的獎品信息 applicationContext.publishEvent(new InitPrizeToRedisEvent(this, lottery.getId(), countDownLatch)); //開始抽獎 lotteryItem = doPlay(lottery); //記錄獎品并扣減庫存 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); //調(diào)整庫存及記錄中獎信息 AbstractRewardProcessor.rewardProcessorMap.get(prizeType).doReward(context); } catch (UnRewardException u) { //表示因?yàn)槟承﹩栴}未中獎,返回一個(gè)默認(rèn)獎項(xiàng) 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); } //拼接返回?cái)?shù)據(jù) drawDto.setLevel(lotteryItem.getLevel()); drawDto.setPrizeName(context.getPrizeName()); drawDto.setPrizeId(context.getPrizeId()); }
首先我們通過CountDownLatch來保證商品初始化的順序,關(guān)于CountDownLatch可以查看 JUC工具 該文章。
然后我們需要檢驗(yàn)一下活動的有效性,確?;顒游唇Y(jié)束。
檢驗(yàn)活動通過后則通過ApplicationEvent 事件實(shí)現(xiàn)獎品數(shù)據(jù)的加載,將其存入Redis中。或者通過ApplicationRunner在程序啟動時(shí)獲取相關(guān)數(shù)據(jù)。我們這使用的是事件機(jī)制。ApplicationRunner 的相關(guān)代碼在下文我也順便貼出。
事件機(jī)制
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; } }
有了事件機(jī)制,我們還需要一個(gè)監(jiān)聽事件,用來初始化相關(guān)數(shù)據(jù)信息。具體業(yè)務(wù)邏輯大家可以參考下代碼,有相關(guān)的注釋信息,主要就是將數(shù)據(jù)庫中的數(shù)據(jù)添加進(jìn)redis中,需要注意的是,我們?yōu)榱吮WC原子性,是通過HASH來存儲數(shù)據(jù)的,這樣之后庫存扣減的時(shí)候就可以通過opsForHash來保證其原子性。
當(dāng)初始化獎品信息之后,則通過countDown()方法表名執(zhí)行完成,業(yè)務(wù)代碼中線程阻塞的地方可以繼續(xù)執(zhí)行了。
@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"); //已經(jīng)初始化到緩存中了,不需要再次緩存 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); //如果指定的獎品沒有了,會生成一個(gè)默認(rèn)的獎項(xiàng) 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); //保存一個(gè)默認(rèn)獎項(xiàng) 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(); //表示初始化完成 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()); } }
上面部分是通過事件的方法來初始化數(shù)據(jù),下面我們說下ApplicationRunner的方式:
這種方式很簡單,在項(xiàng)目啟動的時(shí)候?qū)?shù)據(jù)加載進(jìn)去即可。
我們只需要實(shí)現(xiàn)ApplicationRunner接口即可,然后在run方法中從數(shù)據(jù)庫讀取數(shù)據(jù)加載到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==========="); //加載當(dāng)前抽獎活動信息 Lottery lottery = lotteryMapper.selectById(1); log.info("=========finish load lottery data to Redis==========="); } }
4.6.3 抽獎
我們在使用事件進(jìn)行數(shù)據(jù)初始化的時(shí)候,可以同時(shí)進(jìn)行抽獎操作,但是注意的是這個(gè)時(shí)候需要使用countDownLatch.await();來阻塞當(dāng)前線程,等待數(shù)據(jù)初始化完成。
在抽獎的過程中,我們首先嘗試從Redis中獲取相關(guān)數(shù)據(jù),如果Redis中沒有則從數(shù)據(jù)庫中加載數(shù)據(jù),如果數(shù)據(jù)庫中也沒查詢到相關(guān)數(shù)據(jù),則表明相關(guān)的數(shù)據(jù)沒有配置完成。
獲取數(shù)據(jù)之后,我們就該開始抽獎了。抽獎的核心在于隨機(jī)性以及概率性,咱們總不能隨便抽抽都能抽到一等獎吧?所以我們需要在表中設(shè)置每個(gè)獎項(xiàng)的概率性。如下所示:
在我們抽獎的時(shí)候需要根據(jù)概率劃分處相關(guān)區(qū)間。我們可以通過Debug的方式來查看一下具體怎么劃分的:
獎項(xiàng)的概率越大,區(qū)間越大;大家看到的順序是不同的,由于我們在上面通過Collections.shuffle(lotteryItems);將集合打亂了,所以這里看到的不是順序展示的。
在生成對應(yīng)區(qū)間后,我們通過生成隨機(jī)數(shù),看隨機(jī)數(shù)落在那個(gè)區(qū)間中,然后將對應(yīng)的獎項(xiàng)返回。這就實(shí)現(xiàn)了我們的抽獎過程。
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; //說明還未加載到緩存中,同步從數(shù)據(jù)庫加載,并且異步將數(shù)據(jù)緩存 if (lotteryItemsObj == null) { lotteryItems = lotteryItemMapper.selectList(queryWrapper); } else { lotteryItems = (List<LotteryItem>) lotteryItemsObj; } //獎項(xiàng)數(shù)據(jù)未配置 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 調(diào)整庫存及記錄
在調(diào)整庫存的時(shí)候,我們需要考慮到每個(gè)獎品類型的不同,根據(jù)不同類型的獎品采取不同的措施。比如如果是一些價(jià)值高昂的獎品,我們需要通過分布式鎖來確保安全性;或者比如有些商品我們需要發(fā)送相應(yīng)的短信;所以我們需要采取一種具有擴(kuò)展性的實(shí)現(xiàn)機(jī)制。
具體的實(shí)現(xiàn)機(jī)制可以看下方的類圖,我首先定義一個(gè)獎品方法的接口(RewardProcessor),然后定義一個(gè)抽象類(AbstractRewardProcessor),抽象類中定義了模板方法,然后我們就可以根據(jù)不同的類型創(chuàng)建不同的處理器即可,這大大加強(qiáng)了我們的擴(kuò)展性。
比如我們這邊就創(chuàng)建了庫存充足處理器及庫存不足處理器。
接口:
public interface RewardProcessor<T> { void doReward(RewardContext context); }
抽象類:
@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); /** * 發(fā)放對應(yīng)的獎品 * * @param context */ protected abstract void processor(RewardContext context); /** * 返回當(dāng)前獎品類型 * * @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)); } }
我們可以從抽象類中的doReward方法處開始查看,比如我們這邊先查看庫存充足處理器中的代碼:
庫存處理器執(zhí)行的時(shí)候首相將Redis中對應(yīng)的獎項(xiàng)庫存減1,這時(shí)候是不需要加鎖的,因?yàn)檫@個(gè)操作是原子性的。
當(dāng)扣減后,我們根據(jù)返回的值判斷商品庫存是否充足,這個(gè)時(shí)候庫存不足則提示未中獎或者返回一個(gè)默認(rèn)商品。
最后我們還需要記得更新下數(shù)據(jù)庫中的相關(guān)數(shù)據(jù)。
@Override protected void processor(RewardContext context) { //扣減庫存(redis的更新) Long result = redisTemplate.opsForHash().increment(context.getKey(), "validStock", -1); //當(dāng)前獎品庫存不足,提示未中獎,或者返回一個(gè)兜底的獎品 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()); //更新庫存(數(shù)據(jù)庫的更新) lotteryPrizeMapper.updateValidStock(context.getPrizeId()); }
方法執(zhí)行完成之后,我們需要執(zhí)行afterProcessor方法:
這個(gè)地方我們是通過異步任務(wù)異步存入抽獎記錄信息。
@Override protected void afterProcessor(RewardContext context) { asyncLotteryRecordTask.saveLotteryRecord(context.getAccountIp(), context.getLotteryItem(), context.getPrizeName()); }
在這邊我們可以發(fā)現(xiàn)是通過Async注解,指定一個(gè)線程池,開啟一個(gè)異步執(zhí)行的方法。
@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); } }
創(chuàng)建一個(gè)線程池:相關(guān)的配置信息是我們定義在YML文件中的數(shù)據(jù)。
@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 總結(jié)
以上便是整個(gè)項(xiàng)目的搭建,關(guān)于前端界面無非就是向后端發(fā)起請求,根據(jù)返回的獎品信息,將指針落在對應(yīng)的轉(zhuǎn)盤位置處,具體代碼可以前往項(xiàng)目地址查看。希望大家可以動個(gè)小手點(diǎn)點(diǎn)贊,嘻嘻。
5. 項(xiàng)目地址
如果直接使用項(xiàng)目的話,記得修改數(shù)據(jù)庫中活動的結(jié)束時(shí)間。
Redis
具體的實(shí)戰(zhàn)項(xiàng)目在lottery工程中。
到此這篇關(guān)于Redis 抽獎大轉(zhuǎn)盤的實(shí)戰(zhàn)示例的文章就介紹到這了,更多相關(guān)Redis 抽獎大轉(zhuǎn)盤內(nèi)容請搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。