<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot實現(xiàn)抽獎大轉盤

          共 21206字,需瀏覽 43分鐘

           ·

          2022-02-11 20:41

          1、項目介紹

          這是一個基于Spring boot + Mybatis Plus + Redis 的簡單案例。

          主要是將活動內(nèi)容、獎品信息、記錄信息等緩存到Redis中,然后所有的抽獎過程全部從Redis中做數(shù)據(jù)的操作。

          大致內(nèi)容很簡單,具體操作下面慢慢分析。

          2、項目演示

          話不多說,首先上圖看看項目效果,如果覺得還行的話咱們就來看看他具體是怎么實現(xiàn)的。

          3、表結構

          該項目包含以下四張表,分別是活動表、獎項表、獎品表以及中獎記錄表。具體的SQL會在文末給出。

          4、項目搭建

          咱們首先先搭建一個標準的Spring boot 項目,直接IDEA創(chuàng)建,然后選擇一些相關的依賴即可。

          4.1 依賴

          該項目主要用到了:Redis,thymeleaf,mybatis-plus等依賴。



          ????
          ????????org.springframework.boot
          ????????spring-boot-starter-data-redis
          ????


          ????
          ????????org.springframework.boot
          ????????spring-boot-starter-thymeleaf
          ????


          ????
          ????????org.springframework.boot
          ????????spring-boot-starter-web
          ????


          ????
          ????????mysql
          ????????mysql-connector-java
          ????????runtime
          ????


          ????
          ????????org.springframework.boot
          ????????spring-boot-starter-test
          ????????test
          ????


          ????
          ????????com.baomidou
          ????????mybatis-plus-boot-starter
          ????????3.4.3
          ????


          ????
          ????????com.baomidou
          ????????mybatis-plus-generator
          ????????3.4.1
          ????


          ????
          ????????com.alibaba
          ????????fastjson
          ????????1.2.72
          ????


          ????
          ????????com.alibaba
          ????????druid-spring-boot-starter
          ????????1.1.22
          ????


          ????
          ????????org.apache.commons
          ????????commons-lang3
          ????????3.9
          ????


          ????
          ????????org.projectlombok
          ????????lombok
          ????????1.18.12
          ????


          ????
          ????????org.apache.commons
          ????????commons-pool2
          ????????2.8.0
          ????


          ????
          ????????org.mapstruct
          ????????mapstruct
          ????????1.4.2.Final
          ????


          ????
          ????????org.mapstruct
          ????????mapstruct-jdk8
          ????????1.4.2.Final
          ????


          ????
          ????????org.mapstruct
          ????????mapstruct-processor
          ????????1.4.2.Final
          ????


          ????
          ????????joda-time
          ????????joda-time
          ????????2.10.6
          ????


          4.2 YML配置

          依賴引入之后,我們需要進行相應的配置:數(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的代碼生成器幫助我們生成一些基礎的業(yè)務代碼,避免這些重復的體力活。

          這邊貼出相關代碼,直接修改數(shù)據(jù)庫連接信息、相關包名模塊名即可。

          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);
          ????????//實體屬性?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 的話,需要添加相關配置,將其注入到Spring容器中。

          @Configuration
          public?class?RedisTemplateConfig?{
          ????@Bean
          ????public?RedisTemplate?redisTemplate(RedisConnectionFactory?redisConnectionFactory)?{
          ????????RedisTemplate?redisTemplate?=?new?RedisTemplate<>();
          ????????redisTemplate.setConnectionFactory(redisConnectionFactory);
          ????????//?使用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);
          ????????//?設置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?{
          ????@Override
          ????public?void?serialize(DateTime?dateTime,?JsonGenerator?jsonGenerator,?SerializerProvider?serializerProvider)?throws?IOException?{
          ????????jsonGenerator.writeString(dateTime.toString("yyyy-MM-dd?HH:mm:ss"));
          ????}
          }

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

          4.5 常量管理

          由于代碼中會用到一些共有的常量,我們應該將其抽離出來。

          public?class?LotteryConstants?{

          ????/**
          ?????*?表示正在抽獎的用戶標記
          ?????*/
          ????public?final?static?String?DRAWING?=?"DRAWING";
          ????
          ????/**
          ?????*?活動標記?LOTTERY:lotteryID
          ?????*/
          ????public?final?static?String?LOTTERY?=?"LOTTERY";
          ????
          ????/**
          ?????*?獎品數(shù)據(jù)??LOTTERY_PRIZE:lotteryID:PrizeId
          ?????*/
          ????public?final?static?String?LOTTERY_PRIZE?=?"LOTTERY_PRIZE";
          ????
          ????/**
          ?????*?默認獎品數(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;
          ????????}
          ????}

          ????/**
          ?????*?獎項緩存:LOTTERY_ITEM:LOTTERY_ID
          ?????*/
          ????public?final?static?String?LOTTERY_ITEM?=?"LOTTERY_ITEM";
          ????
          ????/**
          ?????*?默認獎項:DEFAULT_LOTTERY_ITEM:LOTTERY_ID
          ?????*/
          ????public?final?static?String?DEFAULT_LOTTERY_ITEM?=?"DEFAULT_LOTTERY_ITEM";

          }
          public?enum?ReturnCodeEnum?{

          ????SUCCESS("0000",?"成功"),

          ????LOTTER_NOT_EXIST("9001",?"指定抽獎活動不存在"),

          ????LOTTER_FINISH("9002",?"活動已結束"),

          ????LOTTER_REPO_NOT_ENOUGHT("9003",?"當前獎品庫存不足"),

          ????LOTTER_ITEM_NOT_INITIAL("9004",?"獎項數(shù)據(jù)未初始化"),

          ????LOTTER_DRAWING("9005",?"上一次抽獎還未結束"),

          ????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進行統(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è)務代碼

          4.6.1 抽獎接口

          我們首先編寫抽獎接口,根據(jù)前臺傳的參數(shù)查詢到具體的活動,然后進行相應的操作。(當然,前端直接是寫死的/lottery/1)

          @GetMapping("/{id}")
          public?ResultResp?doDraw(@PathVariable("id")?Integer?id,?HttpServletRequest?request)?{
          ????String?accountIp?=?CusAccessObjectUtil.getIpAddress(request);
          ????log.info("begin?LotteryController.doDraw,access?user?{},?lotteryId,{}:",?accountIp,?id);
          ????ResultResp?resultResp?=?new?ResultResp<>();
          ????try?{
          ????????//判斷當前用戶上一次抽獎是否結束
          ????????checkDrawParams(id,?accountIp);

          ????????//抽獎
          ????????DoDrawDto?dto?=?new?DoDrawDto();
          ????????dto.setAccountIp(accountIp);
          ????????dto.setLotteryId(id);
          ????????lotteryService.doDraw(dto);

          ????????//返回結果設置
          ????????resultResp.setCode(ReturnCodeEnum.SUCCESS.getCode());
          ????????resultResp.setMsg(ReturnCodeEnum.SUCCESS.getMsg());
          ????????//對象轉換
          ????????resultResp.setResult(lotteryConverter.dto2LotteryItemVo(dto));
          ????}?catch?(Exception?e)?{
          ????????return?ExceptionUtil.handlerException4biz(resultResp,?e);
          ????}?finally?{
          ????????//清除占位標記
          ????????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命令,判斷當前用戶上一次抽獎是否結束
          ????Boolean?result?=?redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getDrawingRedisKey(accountIp),?"1",?60,?TimeUnit.SECONDS);
          ????//如果為false,說明上一次抽獎還未結束
          ????if?(!result)?{
          ????????throw?new?RewardException(ReturnCodeEnum.LOTTER_DRAWING.getCode(),?ReturnCodeEnum.LOTTER_DRAWING.getMsg());
          ????}
          }

          為了避免用戶重復點擊抽獎,所以我們通過Redis來避免這種問題,用戶每次抽獎的時候,通過setNx給用戶排隊并設置過期時間;如果用戶點擊多次抽獎,Redis設置值的時候發(fā)現(xiàn)該用戶上次抽獎還未結束則拋出異常。

          最后用戶抽獎成功的話,記得清除該標記,從而用戶能夠繼續(xù)抽獎。

          4.6.2 初始化數(shù)據(jù)

          從抽獎入口進來,校驗成功以后則開始業(yè)務操作。

          @Override
          public?void?doDraw(DoDrawDto?drawDto)?throws?Exception?{
          ????RewardContext?context?=?new?RewardContext();
          ????LotteryItem?lotteryItem?=?null;
          ????try?{
          ????????//JUC工具?需要等待線程結束之后才能運行
          ????????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);
          ????????//調整庫存及記錄中獎信息
          ????????AbstractRewardProcessor.rewardProcessorMap.get(prizeType).doReward(context);
          ????}?catch?(UnRewardException?u)?{?//表示因為某些問題未中獎,返回一個默認獎項
          ????????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);
          ????}
          ????//拼接返回數(shù)據(jù)
          ????drawDto.setLevel(lotteryItem.getLevel());
          ????drawDto.setPrizeName(context.getPrizeName());
          ????drawDto.setPrizeId(context.getPrizeId());
          }

          首先我們通過CountDownLatch來保證商品初始化的順序,關于CountDownLatch可以查看 JUC工具 該文章。

          然后我們需要檢驗一下活動的有效性,確?;顒游唇Y束。

          檢驗活動通過后則通過ApplicationEvent 事件實現(xiàn)獎品數(shù)據(jù)的加載,將其存入Redis中?;蛘咄ㄟ^ApplicationRunner在程序啟動時獲取相關數(shù)據(jù)。我們這使用的是事件機制。ApplicationRunner 的相關代碼在下文我也順便貼出。

          事件機制

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

          }

          有了事件機制,我們還需要一個監(jiān)聽事件,用來初始化相關數(shù)據(jù)信息。具體業(yè)務邏輯大家可以參考下代碼,有相關的注釋信息,主要就是將數(shù)據(jù)庫中的數(shù)據(jù)添加進redis中,需要注意的是,我們?yōu)榱吮WC原子性,是通過HASH來存儲數(shù)據(jù)的,這樣之后庫存扣減的時候就可以通過opsForHash來保證其原子性。

          當初始化獎品信息之后,則通過countDown()方法表名執(zhí)行完成,業(yè)務代碼中線程阻塞的地方可以繼續(xù)執(zhí)行了。

          @Slf4j
          @Component
          public?class?InitPrizeToRedisListener?implements?ApplicationListener?{

          ????@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?lotteryItemQueryWrapper?=?new?QueryWrapper<>();
          ????????lotteryItemQueryWrapper.eq("lottery_id",?initPrizeToRedisEvent.getLotteryId());
          ????????List?lotteryItems?=?lotteryItemMapper.selectList(lotteryItemQueryWrapper);

          ????????//如果指定的獎品沒有了,會生成一個默認的獎項
          ????????LotteryItem?defaultLotteryItem?=?lotteryItems.parallelStream().filter(o?->?o.getDefaultItem().intValue()?==?1).findFirst().orElse(null);

          ????????Map?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?lotteryPrizes?=?lotteryPrizeMapper.selectList(queryWrapper);

          ????????//保存一個默認獎項
          ????????AtomicReference?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的方式:

          這種方式很簡單,在項目啟動的時候將數(shù)據(jù)加載進去即可。

          我們只需要實現(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===========");
          ????????//加載當前抽獎活動信息
          ????????Lottery?lottery?=?lotteryMapper.selectById(1);

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

          4.6.3 抽獎

          我們在使用事件進行數(shù)據(jù)初始化的時候,可以同時進行抽獎操作,但是注意的是這個時候需要使用countDownLatch.await();來阻塞當前線程,等待數(shù)據(jù)初始化完成。

          在抽獎的過程中,我們首先嘗試從Redis中獲取相關數(shù)據(jù),如果Redis中沒有則從數(shù)據(jù)庫中加載數(shù)據(jù),如果數(shù)據(jù)庫中也沒查詢到相關數(shù)據(jù),則表明相關的數(shù)據(jù)沒有配置完成。

          獲取數(shù)據(jù)之后,我們就該開始抽獎了。抽獎的核心在于隨機性以及概率性,咱們總不能隨便抽抽都能抽到一等獎吧?所以我們需要在表中設置每個獎項的概率性。如下所示:

          在我們抽獎的時候需要根據(jù)概率劃分處相關區(qū)間。我們可以通過Debug的方式來查看一下具體怎么劃分的:

          獎項的概率越大,區(qū)間越大;大家看到的順序是不同的,由于我們在上面通過Collections.shuffle(lotteryItems);將集合打亂了,所以這里看到的不是順序展示的。

          在生成對應區(qū)間后,我們通過生成隨機數(shù),看隨機數(shù)落在那個區(qū)間中,然后將對應的獎項返回。這就實現(xiàn)了我們的抽獎過程。

          private?LotteryItem?doPlay(Lottery?lottery)?{
          ????LotteryItem?lotteryItem?=?null;
          ????QueryWrapper?queryWrapper?=?new?QueryWrapper<>();
          ????queryWrapper.eq("lottery_id",?lottery.getId());
          ????Object?lotteryItemsObj?=?redisTemplate.opsForValue().get(RedisKeyManager.getLotteryItemRedisKey(lottery.getId()));
          ????List?lotteryItems;
          ????//說明還未加載到緩存中,同步從數(shù)據(jù)庫加載,并且異步將數(shù)據(jù)緩存
          ????if?(lotteryItemsObj?==?null)?{
          ????????lotteryItems?=?lotteryItemMapper.selectList(queryWrapper);
          ????}?else?{
          ????????lotteryItems?=?(List)?lotteryItemsObj;
          ????}
          ????//獎項數(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?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>?set?=?awardItemScope.entrySet();
          ????????for?(Map.Entry?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 調整庫存及記錄

          在調整庫存的時候,我們需要考慮到每個獎品類型的不同,根據(jù)不同類型的獎品采取不同的措施。比如如果是一些價值高昂的獎品,我們需要通過分布式鎖來確保安全性;或者比如有些商品我們需要發(fā)送相應的短信;所以我們需要采取一種具有擴展性的實現(xiàn)機制。微信搜索公眾號:Java項目精選,回復:java 領取資料 。

          具體的實現(xiàn)機制可以看下方的類圖,我首先定義一個獎品方法的接口(RewardProcessor),然后定義一個抽象類(AbstractRewardProcessor),抽象類中定義了模板方法,然后我們就可以根據(jù)不同的類型創(chuàng)建不同的處理器即可,這大大加強了我們的擴展性。

          比如我們這邊就創(chuàng)建了庫存充足處理器及庫存不足處理器。

          接口:

          public?interface?RewardProcessor?{

          ????void?doReward(RewardContext?context);

          }

          抽象類:

          @Slf4j
          public?abstract?class?AbstractRewardProcessor?implements?RewardProcessor,?ApplicationContextAware?{

          ????public?static?Map?rewardProcessorMap?=?new?ConcurrentHashMap();

          ????@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ā)放對應的獎品
          ?????*
          ?????*?@param?context
          ?????*/
          ????protected?abstract?void?processor(RewardContext?context);

          ????/**
          ?????*?返回當前獎品類型
          ?????*
          ?????*?@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í)行的時候首相將Redis中對應的獎項庫存減1,這時候是不需要加鎖的,因為這個操作是原子性的。

          當扣減后,我們根據(jù)返回的值判斷商品庫存是否充足,這個時候庫存不足則提示未中獎或者返回一個默認商品。

          最后我們還需要記得更新下數(shù)據(jù)庫中的相關數(shù)據(jù)。

          @Override
          protected?void?processor(RewardContext?context)?{
          ????//扣減庫存(redis的更新)
          ????Long?result?=?redisTemplate.opsForHash().increment(context.getKey(),?"validStock",?-1);
          ????//當前獎品庫存不足,提示未中獎,或者返回一個兜底的獎品
          ????if?(result.intValue()?????????throw?new?UnRewardException(ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getCode(),?ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getMsg());
          ????}
          ????List?propertys?=?Arrays.asList("id",?"prizeName");
          ????List?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方法:

          這個地方我們是通過異步任務異步存入抽獎記錄信息。

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

          在這邊我們可以發(fā)現(xiàn)是通過Async注解,指定一個線程池,開啟一個異步執(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)建一個線程池:相關的配置信息是我們定義在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 總結

          以上便是整個項目的搭建,關于前端界面無非就是向后端發(fā)起請求,根據(jù)返回的獎品信息,將指針落在對應的轉盤位置處,具體代碼可以前往項目地址查看。希望大家可以動個小手點點贊,嘻嘻。

          5. 項目地址

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

          如果直接使用項目的話,記得修改數(shù)據(jù)庫中活動的結束時間。

          具體的實戰(zhàn)項目在lottery工程中。

          (完)


          入骨相思知不知


          玲瓏骰子安紅豆


          入我相思門,知我相思苦,長相思兮長相憶,短相思兮無窮極。

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    嫩逼网站 | 国产无码精品黄色电影 | 超碰人人奸| 中国一级免费黄色片 | 天堂AV资源在线 |