欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Redis?抽獎大轉盤的實戰(zhàn)示例

 更新時間:2021年12月31日 09:09:44   作者:、楽.  
本文主要介紹了Redis?抽獎大轉盤的實戰(zhàn)示例,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下

1. 項目介紹

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

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

大致內容很簡單,具體操作下面慢慢分析。

2. 項目演示

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

image-20211229100617994

image-20211229101138854

3. 表結構

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

image-20211229095750532

4. 項目搭建

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

4.1 依賴

該項目主要用到了: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配置

依賴引入之后,我們需要進行相應的配置:數(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 配置

我們如果在代碼中使用 RedisTemplate 的話,需要添加相關配置,將其注入到Spring容器中。

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

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

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<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 {
        //判斷當前用戶上一次抽獎是否結束
        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<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);

        //如果指定的獎品沒有了,會生成一個默認的獎項
        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);

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

這種方式很簡單,在項目啟動的時候將數(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ù)之后,我們就該開始抽獎了。抽獎的核心在于隨機性以及概率性,咱們總不能隨便抽抽都能抽到一等獎吧?所以我們需要在表中設置每個獎項的概率性。如下所示:

image-20211229212035238

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

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

image-20211229212634205

在生成對應區(qū)間后,我們通過生成隨機數(shù),看隨機數(shù)落在那個區(qū)間中,然后將對應的獎項返回。這就實現(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;
    }
    //獎項數(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 調整庫存及記錄

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

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

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

image-20211229214246943

image-20211229214223549

接口:

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ā)放對應的獎品
     *
     * @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() < 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方法:

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

@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. 項目地址

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

Redis

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

image-20211229221247136

到此這篇關于Redis 抽獎大轉盤的實戰(zhàn)示例的文章就介紹到這了,更多相關Redis 抽獎大轉盤內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Redis和MySQL保證雙寫一致性的問題解析

    Redis和MySQL保證雙寫一致性的問題解析

    Redis和MySQL的雙寫一致性指的是在同時使用緩存和數(shù)據(jù)庫存儲數(shù)據(jù)的時候,保證Redis和MySQL中數(shù)據(jù)的一致性,那么如何才能保證他們的一致性呢,下面小編就來為大家詳細講講
    2023-11-11
  • Redis獲取某個前綴的key腳本實例

    Redis獲取某個前綴的key腳本實例

    這篇文章主要給大家介紹了關于Redis獲取某個前綴的key腳本的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Redis具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2018-04-04
  • Redis五種數(shù)據(jù)類型詳解

    Redis五種數(shù)據(jù)類型詳解

    Redis是基于內存的 K-V 數(shù)據(jù)庫,常用于緩存、消息隊列,分布式鎖等場景,并且提供了常見的數(shù)據(jù)結構:字符串、哈希、列表、集合、帶排序的集合,本文主要介紹了Redis的五種數(shù)據(jù)類型,感興趣的小伙伴可以參考閱讀本文
    2023-04-04
  • 使用redis生成唯一編號及原理示例詳解

    使用redis生成唯一編號及原理示例詳解

    今天介紹下如何使用redis生成唯一的序列號,其實主要思想還是利用redis單線程的特性,可以保證操作的原子性,使讀寫同一個key時不會出現(xiàn)不同的數(shù)據(jù),感興趣的朋友跟隨小編一起看看吧
    2021-09-09
  • Redis 數(shù)據(jù)類型的詳解

    Redis 數(shù)據(jù)類型的詳解

    這篇文章主要介紹了Redis 數(shù)據(jù)類型的詳解的相關資料,支持五種數(shù)據(jù)類型,字符串,哈希,列表,集合及zset,需要的朋友可以參考下
    2017-08-08
  • redis過期回調坑的解決

    redis過期回調坑的解決

    Redis提供了一種過期回調的機制,可以在某個鍵過期時觸發(fā)一個回調函數(shù),然而,在實際使用中,我們往往會遇到一些災難性的問題,其中一個就是在使用過期回調的時候,我們可能會遭遇到無法預料的錯誤,本文就詳細的介紹一下
    2023-09-09
  • 淺談Redis內存回收策略

    淺談Redis內存回收策略

    本文主要介紹了淺談Redis內存回收策略,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-06-06
  • 淺談Redis中的內存淘汰策略和過期鍵刪除策略

    淺談Redis中的內存淘汰策略和過期鍵刪除策略

    本文主要介紹了淺談Redis中的內存淘汰策略和過期鍵刪除策略,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • 淺談一下Redis的數(shù)據(jù)結構

    淺談一下Redis的數(shù)據(jù)結構

    這篇文章主要介紹了淺談一下Redis的數(shù)據(jù)結構,簡單字符串結構被用于存儲redis的key對象和String類型的value對象,其中的free和len字段可以輕松的使得在該字符串被修改時判斷是否需要擴容,需要的朋友可以參考下
    2023-08-08
  • Reactor?WebFlux集成Redis處理緩存操作

    Reactor?WebFlux集成Redis處理緩存操作

    這篇文章主要為大家介紹了Reactor?WebFlux集成Redis處理緩存操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-09-09

最新評論