1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot實(shí)現(xiàn)抽獎(jiǎng)大轉(zhuǎn)盤

        共 21206字,需瀏覽 43分鐘

         ·

        2022-02-09 18:39

        1、項(xiàng)目介紹

        這是一個(gè)基于Spring boot + Mybatis Plus + Redis 的簡(jiǎn)單案例。

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

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

        2、項(xiàng)目演示

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

        3、表結(jié)構(gòu)

        該項(xiàng)目包含以下四張表,分別是活動(dòng)表、獎(jiǎng)項(xiàng)表、獎(jiǎng)品表以及中獎(jiǎng)記錄表。具體的SQL會(huì)在文末給出。

        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等依賴。



        ????
        ????????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配置

        依賴引入之后,我們需要進(jìn)行相應(yīng)的配置:數(shù)據(jù)庫(kù)連接信息、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ù)庫(kù)連接信息、相關(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("請(qǐng)輸入"?+?tip?+?":");
        ????????System.out.println(help.toString());
        ????????if?(scanner.hasNext())?{
        ????????????String?ipt?=?scanner.next();
        ????????????if?(StringUtils.isNotEmpty(ipt))?{
        ????????????????return?ipt;
        ????????????}
        ????????}
        ????????throw?new?MybatisPlusException("請(qǐng)輸入正確的"?+?tip?+?"!");
        ????}
        }

        4.4 Redis 配置

        我們?nèi)绻诖a中使用 RedisTemplate 的話,需要添加相關(guān)配置,將其注入到Spring容器中。

        @Configuration
        public?class?RedisTemplateConfig?{
        ????@Bean
        ????public?RedisTemplate?redisTemplate(RedisConnectionFactory?redisConnectionFactory)?{
        ????????RedisTemplate?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?{
        ????@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 常量管理

        由于代碼中會(huì)用到一些共有的常量,我們應(yīng)該將其抽離出來(lái)。

        public?class?LotteryConstants?{

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

        ????/**
        ?????*?獎(jiǎng)項(xiàng)緩存:LOTTERY_ITEM:LOTTERY_ID
        ?????*/
        ????public?final?static?String?LOTTERY_ITEM?=?"LOTTERY_ITEM";
        ????
        ????/**
        ?????*?默認(rèn)獎(jiǎng)項(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",?"指定抽獎(jiǎng)活動(dòng)不存在"),

        ????LOTTER_FINISH("9002",?"活動(dòng)已結(jié)束"),

        ????LOTTER_REPO_NOT_ENOUGHT("9003",?"當(dāng)前獎(jiǎng)品庫(kù)存不足"),

        ????LOTTER_ITEM_NOT_INITIAL("9004",?"獎(jiǎng)項(xiàng)數(shù)據(jù)未初始化"),

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

        ????REQUEST_PARAM_NOT_VALID("9998",?"請(qǐng)求參數(shù)不正確"),

        ????SYSTEM_ERROR("9999",?"系統(tǒng)繁忙,請(qǐ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()?+?"";
        ????}
        }

        對(duì)Redis中的key進(jìn)行統(tǒng)一的管理。

        public?class?RedisKeyManager?{

        ????/**
        ?????*?正在抽獎(jiǎng)的key
        ?????*
        ?????*?@param?accountIp
        ?????*?@return
        ?????*/
        ????public?static?String?getDrawingRedisKey(String?accountIp)?{
        ????????return?new?StringBuilder(LotteryConstants.DRAWING).append(":").append(accountIp).toString();
        ????}

        ????/**
        ?????*?獲取抽獎(jiǎng)活動(dòng)的key
        ?????*
        ?????*?@param?id
        ?????*?@return
        ?????*/
        ????public?static?String?getLotteryRedisKey(Integer?id)?{
        ????????return?new?StringBuilder(LotteryConstants.LOTTERY).append(":").append(id).toString();
        ????}

        ????/**
        ?????*?獲取指定活動(dòng)下的所有獎(jiǎng)品數(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 抽獎(jiǎng)接口

        我們首先編寫抽獎(jiǎng)接口,根據(jù)前臺(tái)傳的參數(shù)查詢到具體的活動(dòng),然后進(jìn)行相應(yīng)的操作。(當(dāng)然,前端直接是寫死的/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?{
        ????????//判斷當(dāng)前用戶上一次抽獎(jiǎng)是否結(jié)束
        ????????checkDrawParams(id,?accountIp);

        ????????//抽獎(jiǎng)
        ????????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());
        ????????//對(duì)象轉(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ǎng)是否結(jié)束
        ????Boolean?result?=?redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getDrawingRedisKey(accountIp),?"1",?60,?TimeUnit.SECONDS);
        ????//如果為false,說(shuō)明上一次抽獎(jiǎng)還未結(jié)束
        ????if?(!result)?{
        ????????throw?new?RewardException(ReturnCodeEnum.LOTTER_DRAWING.getCode(),?ReturnCodeEnum.LOTTER_DRAWING.getMsg());
        ????}
        }

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

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

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

        從抽獎(jiǎng)入口進(jìn)來(lái),校驗(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);
        ????????//判斷活動(dòng)有效性
        ????????Lottery?lottery?=?checkLottery(drawDto);
        ????????//發(fā)布事件,用來(lái)加載指定活動(dòng)的獎(jiǎng)品信息
        ????????applicationContext.publishEvent(new?InitPrizeToRedisEvent(this,?lottery.getId(),?countDownLatch));
        ????????//開始抽獎(jiǎng)
        ????????lotteryItem?=?doPlay(lottery);
        ????????//記錄獎(jiǎng)品并扣減庫(kù)存
        ????????countDownLatch.await();?//等待獎(jiǎng)品初始化完成
        ????????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)整庫(kù)存及記錄中獎(jiǎng)信息
        ????????AbstractRewardProcessor.rewardProcessorMap.get(prizeType).doReward(context);
        ????}?catch?(UnRewardException?u)?{?//表示因?yàn)槟承﹩栴}未中獎(jiǎng),返回一個(gè)默認(rèn)獎(jiǎng)項(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來(lái)保證商品初始化的順序,關(guān)于CountDownLatch可以查看 JUC工具 該文章。

        然后我們需要檢驗(yàn)一下活動(dòng)的有效性,確保活動(dòng)未結(jié)束。

        檢驗(yàn)活動(dòng)通過后則通過ApplicationEvent 事件實(shí)現(xiàn)獎(jiǎng)品數(shù)據(jù)的加載,將其存入Redis中?;蛘咄ㄟ^ApplicationRunner在程序啟動(dòng)時(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)聽事件,用來(lái)初始化相關(guān)數(shù)據(jù)信息。具體業(yè)務(wù)邏輯大家可以參考下代碼,有相關(guān)的注釋信息,主要就是將數(shù)據(jù)庫(kù)中的數(shù)據(jù)添加進(jìn)redis中,需要注意的是,我們?yōu)榱吮WC原子性,是通過HASH來(lái)存儲(chǔ)數(shù)據(jù)的,這樣之后庫(kù)存扣減的時(shí)候就可以通過opsForHash來(lái)保證其原子性。

        當(dāng)初始化獎(jiǎng)品信息之后,則通過countDown()方法表名執(zhí)行完成,業(yè)務(wù)代碼中線程阻塞的地方可以繼續(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);

        ????????//如果指定的獎(jiǎng)品沒有了,會(huì)生成一個(gè)默認(rèn)的獎(jiǎng)項(xiàng)
        ????????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);

        ????????//保存一個(gè)默認(rèn)獎(jiǎng)項(xiàng)
        ????????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());
        ????}
        }

        上面部分是通過事件的方法來(lái)初始化數(shù)據(jù),下面我們說(shuō)下ApplicationRunner的方式:

        這種方式很簡(jiǎn)單,在項(xiàng)目啟動(dòng)的時(shí)候?qū)?shù)據(jù)加載進(jìn)去即可。

        我們只需要實(shí)現(xiàn)ApplicationRunner接口即可,然后在run方法中從數(shù)據(jù)庫(kù)讀取數(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)前抽獎(jiǎng)活動(dòng)信息
        ????????Lottery?lottery?=?lotteryMapper.selectById(1);

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

        4.6.3 抽獎(jiǎng)

        我們?cè)谑褂檬录M(jìn)行數(shù)據(jù)初始化的時(shí)候,可以同時(shí)進(jìn)行抽獎(jiǎng)操作,但是注意的是這個(gè)時(shí)候需要使用countDownLatch.await();來(lái)阻塞當(dāng)前線程,等待數(shù)據(jù)初始化完成。

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

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

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

        獎(jiǎng)項(xiàng)的概率越大,區(qū)間越大;大家看到的順序是不同的,由于我們?cè)谏厦嫱ㄟ^Collections.shuffle(lotteryItems);將集合打亂了,所以這里看到的不是順序展示的。

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

        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;
        ????//說(shuō)明還未加載到緩存中,同步從數(shù)據(jù)庫(kù)加載,并且異步將數(shù)據(jù)緩存
        ????if?(lotteryItemsObj?==?null)?{
        ????????lotteryItems?=?lotteryItemMapper.selectList(queryWrapper);
        ????}?else?{
        ????????lotteryItems?=?(List)?lotteryItemsObj;
        ????}
        ????//獎(jiǎng)項(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?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 調(diào)整庫(kù)存及記錄

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

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

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

        接口:

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

        ????/**
        ?????*?返回當(dāng)前獎(jiǎ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方法處開始查看,比如我們這邊先查看庫(kù)存充足處理器中的代碼:

        庫(kù)存處理器執(zhí)行的時(shí)候首相將Redis中對(duì)應(yīng)的獎(jiǎng)項(xiàng)庫(kù)存減1,這時(shí)候是不需要加鎖的,因?yàn)檫@個(gè)操作是原子性的。

        當(dāng)扣減后,我們根據(jù)返回的值判斷商品庫(kù)存是否充足,這個(gè)時(shí)候庫(kù)存不足則提示未中獎(jiǎng)或者返回一個(gè)默認(rèn)商品。

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

        @Override
        protected?void?processor(RewardContext?context)?{
        ????//扣減庫(kù)存(redis的更新)
        ????Long?result?=?redisTemplate.opsForHash().increment(context.getKey(),?"validStock",?-1);
        ????//當(dāng)前獎(jiǎng)品庫(kù)存不足,提示未中獎(jiǎng),或者返回一個(gè)兜底的獎(jiǎng)品
        ????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());
        ????//更新庫(kù)存(數(shù)據(jù)庫(kù)的更新)
        ????lotteryPrizeMapper.updateValidStock(context.getPrizeId());
        }

        方法執(zhí)行完成之后,我們需要執(zhí)行afterProcessor方法:

        這個(gè)地方我們是通過異步任務(wù)異步存入抽獎(jiǎng)記錄信息。

        @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");
        ????????//存儲(chǔ)中獎(jiǎng)信息
        ????????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)于前端界面無(wú)非就是向后端發(fā)起請(qǐng)求,根據(jù)返回的獎(jiǎng)品信息,將指針落在對(duì)應(yīng)的轉(zhuǎn)盤位置處,具體代碼可以前往項(xiàng)目地址查看。希望大家可以動(dòng)個(gè)小手點(diǎn)點(diǎn)贊,嘻嘻。

        5. 項(xiàng)目地址

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

        如果直接使用項(xiàng)目的話,記得修改數(shù)據(jù)庫(kù)中活動(dòng)的結(jié)束時(shí)間。

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

        (完)

        PS:如果覺得我的分享不錯(cuò),歡迎大家隨手點(diǎn)贊、在看。

        ?關(guān)注公眾號(hào):Java后端編程,回復(fù)下面關(guān)鍵字?


        要Java學(xué)習(xí)完整路線,回復(fù)??路線?

        缺Java入門視頻,回復(fù)?視頻?

        要Java面試經(jīng)驗(yàn),回復(fù)??面試?

        缺Java項(xiàng)目,回復(fù):?項(xiàng)目?

        進(jìn)Java粉絲群:?加群?


        PS:如果覺得我的分享不錯(cuò),歡迎大家隨手點(diǎn)贊、在看。

        (完)




        加我"微信"?獲取一份 最新Java面試題資料

        請(qǐng)備注:666,不然不通過~


        最近好文


        1、再見了,收費(fèi)的XShell,我改用國(guó)產(chǎn)良心工具!

        2、給IDEA換個(gè)酷炫的主題,真的太好看了!

        3、SpringBoot快速開發(fā)利器:Spring Boot CLI

        4、基于SpringBoot 的CMS系統(tǒng),拿去開發(fā)企業(yè)官網(wǎng)

        5、本機(jī)號(hào)碼一鍵登錄原理與應(yīng)用



        最近面試BAT,整理一份面試資料Java面試BAT通關(guān)手冊(cè),覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
        獲取方式:關(guān)注公眾號(hào)并回復(fù)?java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
        明天見(??ω??)??
        瀏覽 56
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            淫色淫色 www.b2gd.com | 日本黄色三级网站 | 午夜精品视频成人精品视频 | 91们嫩草伦理 | 欧美日韩国产免费一区二区三区 | 草比克在线视频 | 韩国国产在线 | 日韩精品无码一区二区三区三区 | www.国产免费 | 91色亚洲|