经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » Redis » 查看文章
Redis?抽奖大转盘的实战示例
来源:jb51  时间:2021/12/31 10:41:12  对本文有异议

1. 项目介绍

这是一个基于Spring boot + Mybatis Plus + Redis 的简单案例。

主要是将活动内容、奖品信息、记录信息等缓存到Redis中,然后所有的抽奖过程全部从Redis中做数据的操作。

大致内容很简单,具体操作下面慢慢分析。

2. 项目演示

话不多说,首先上图看看项目效果,如果觉得还行的话咱们就来看看他具体是怎么实现的。

image-20211229100617994

image-20211229101138854

3. 表结构

该项目包含以下四张表,分别是活动表、奖项表、奖品表以及中奖记录表。具体的SQL会在文末给出。

image-20211229095750532

4. 项目搭建

咱们首先先搭建一个标准的Spring boot 项目,直接IDEA创建,然后选择一些相关的依赖即可。

4.1 依赖

该项目主要用到了:Redis,thymeleaf,mybatis-plus等依赖。

  1. <dependencies>
  2.  
  3. <dependency>
  4. <groupId>org.springframework.boot</groupId>
  5. <artifactId>spring-boot-starter-data-redis</artifactId>
  6. </dependency>
  7.  
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  11. </dependency>
  12.  
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-web</artifactId>
  16. </dependency>
  17.  
  18. <dependency>
  19. <groupId>mysql</groupId>
  20. <artifactId>mysql-connector-java</artifactId>
  21. <scope>runtime</scope>
  22. </dependency>
  23.  
  24. <dependency>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-starter-test</artifactId>
  27. <scope>test</scope>
  28. </dependency>
  29.  
  30. <dependency>
  31. <groupId>com.baomidou</groupId>
  32. <artifactId>mybatis-plus-boot-starter</artifactId>
  33. <version>3.4.3</version>
  34. </dependency>
  35.  
  36. <dependency>
  37. <groupId>com.baomidou</groupId>
  38. <artifactId>mybatis-plus-generator</artifactId>
  39. <version>3.4.1</version>
  40. </dependency>
  41.  
  42. <dependency>
  43. <groupId>com.alibaba</groupId>
  44. <artifactId>fastjson</artifactId>
  45. <version>1.2.72</version>
  46. </dependency>
  47.  
  48. <dependency>
  49. <groupId>com.alibaba</groupId>
  50. <artifactId>druid-spring-boot-starter</artifactId>
  51. <version>1.1.22</version>
  52. </dependency>
  53.  
  54. <dependency>
  55. <groupId>org.apache.commons</groupId>
  56. <artifactId>commons-lang3</artifactId>
  57. <version>3.9</version>
  58. </dependency>
  59.  
  60. <dependency>
  61. <groupId>org.projectlombok</groupId>
  62. <artifactId>lombok</artifactId>
  63. <version>1.18.12</version>
  64. </dependency>
  65.  
  66. <dependency>
  67. <groupId>org.apache.commons</groupId>
  68. <artifactId>commons-pool2</artifactId>
  69. <version>2.8.0</version>
  70. </dependency>
  71.  
  72. <dependency>
  73. <groupId>org.mapstruct</groupId>
  74. <artifactId>mapstruct</artifactId>
  75. <version>1.4.2.Final</version>
  76. </dependency>
  77.  
  78. <dependency>
  79. <groupId>org.mapstruct</groupId>
  80. <artifactId>mapstruct-jdk8</artifactId>
  81. <version>1.4.2.Final</version>
  82. </dependency>
  83.  
  84. <dependency>
  85. <groupId>org.mapstruct</groupId>
  86. <artifactId>mapstruct-processor</artifactId>
  87. <version>1.4.2.Final</version>
  88. </dependency>
  89.  
  90. <dependency>
  91. <groupId>joda-time</groupId>
  92. <artifactId>joda-time</artifactId>
  93. <version>2.10.6</version>
  94. </dependency>
  95. </dependencies>

4.2 YML配置

依赖引入之后,我们需要进行相应的配置:数据库连接信息、Redis、mybatis-plus、线程池等。

  1. server:
  2. port: 8080
  3. servlet:
  4. context-path: /
  5. spring:
  6. datasource:
  7. druid:
  8. url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
  9. username: root
  10. password: 123456
  11. driver-class-name: com.mysql.cj.jdbc.Driver
  12. initial-size: 30
  13. max-active: 100
  14. min-idle: 10
  15. max-wait: 60000
  16. time-between-eviction-runs-millis: 60000
  17. min-evictable-idle-time-millis: 300000
  18. validation-query: SELECT 1 FROM DUAL
  19. test-while-idle: true
  20. test-on-borrow: false
  21. test-on-return: false
  22. filters: stat,wall
  23. redis:
  24. port: 6379
  25. host: 127.0.0.1
  26. lettuce:
  27. pool:
  28. max-active: -1
  29. max-idle: 2000
  30. max-wait: -1
  31. min-idle: 1
  32. time-between-eviction-runs: 5000
  33. mvc:
  34. view:
  35. prefix: classpath:/templates/
  36. suffix: .html
  37. # mybatis-plus
  38. mybatis-plus:
  39. configuration:
  40. map-underscore-to-camel-case: true
  41. auto-mapping-behavior: full
  42. mapper-locations: classpath*:mapper/**/*Mapper.xml
  43.  
  44. # 线程池
  45. async:
  46. executor:
  47. thread:
  48. core-pool-size: 6
  49. max-pool-size: 12
  50. queue-capacity: 100000
  51. name-prefix: lottery-service-

4.3 代码生成

这边我们可以直接使用mybatis-plus的代码生成器帮助我们生成一些基础的业务代码,避免这些重复的体力活。

这边贴出相关代码,直接修改数据库连接信息、相关包名模块名即可。

  1. public class MybatisPlusGeneratorConfig {
  2. public static void main(String[] args) {
  3. // 代码生成器
  4. AutoGenerator mpg = new AutoGenerator();
  5.  
  6. // 全局配置
  7. GlobalConfig gc = new GlobalConfig();
  8. String projectPath = System.getProperty("user.dir");
  9. gc.setOutputDir(projectPath + "/src/main/java");
  10. gc.setAuthor("chen");
  11. gc.setOpen(false);
  12. //实体属性 Swagger2 注解
  13. gc.setSwagger2(false);
  14. mpg.setGlobalConfig(gc);
  15.  
  16. // 数据源配置
  17. DataSourceConfig dsc = new DataSourceConfig();
  18. dsc.setUrl("jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
  19. dsc.setDriverName("com.mysql.cj.jdbc.Driver");
  20. dsc.setUsername("root");
  21. dsc.setPassword("123456");
  22. mpg.setDataSource(dsc);
  23.  
  24. // 包配置
  25. PackageConfig pc = new PackageConfig();
  26. // pc.setModuleName(scanner("模块名"));
  27. pc.setParent("com.example.lottery");
  28. pc.setEntity("dal.model");
  29. pc.setMapper("dal.mapper");
  30. pc.setService("service");
  31. pc.setServiceImpl("service.impl");
  32. mpg.setPackageInfo(pc);
  33.  
  34.  
  35. // 配置模板
  36. TemplateConfig templateConfig = new TemplateConfig();
  37.  
  38. templateConfig.setXml(null);
  39. mpg.setTemplate(templateConfig);
  40.  
  41. // 策略配置
  42. StrategyConfig strategy = new StrategyConfig();
  43. strategy.setNaming(NamingStrategy.underline_to_camel);
  44. strategy.setColumnNaming(NamingStrategy.underline_to_camel);
  45. strategy.setSuperEntityClass("com.baomidou.mybatisplus.extension.activerecord.Model");
  46. strategy.setEntityLombokModel(true);
  47. strategy.setRestControllerStyle(true);
  48.  
  49. strategy.setEntityLombokModel(true);
  50. // 公共父类
  51. // strategy.setSuperControllerClass("com.baomidou.ant.common.BaseController");
  52. // 写于父类中的公共字段
  53. // strategy.setSuperEntityColumns("id");
  54. strategy.setInclude(scanner("lottery,lottery_item,lottery_prize,lottery_record").split(","));
  55. strategy.setControllerMappingHyphenStyle(true);
  56. strategy.setTablePrefix(pc.getModuleName() + "_");
  57. mpg.setStrategy(strategy);
  58. mpg.setTemplateEngine(new FreemarkerTemplateEngine());
  59. mpg.execute();
  60. }
  61.  
  62. public static String scanner(String tip) {
  63. Scanner scanner = new Scanner(System.in);
  64. StringBuilder help = new StringBuilder();
  65. help.append("请输入" + tip + ":");
  66. System.out.println(help.toString());
  67. if (scanner.hasNext()) {
  68. String ipt = scanner.next();
  69. if (StringUtils.isNotEmpty(ipt)) {
  70. return ipt;
  71. }
  72. }
  73. throw new MybatisPlusException("请输入正确的" + tip + "!");
  74. }
  75. }

4.4 Redis 配置

我们如果在代码中使用 RedisTemplate 的话,需要添加相关配置,将其注入到Spring容器中。

  1. @Configuration
  2. public class RedisTemplateConfig {
  3. @Bean
  4. public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  5. RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
  6. redisTemplate.setConnectionFactory(redisConnectionFactory);
  7. // 使用Jackson2JsonRedisSerialize 替换默认序列化
  8. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  9.  
  10. ObjectMapper objectMapper = new ObjectMapper();
  11. objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  12. objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  13.  
  14. SimpleModule simpleModule = new SimpleModule();
  15. simpleModule.addSerializer(DateTime.class, new JodaDateTimeJsonSerializer());
  16. simpleModule.addDeserializer(DateTime.class, new JodaDateTimeJsonDeserializer());
  17. objectMapper.registerModule(simpleModule);
  18.  
  19. jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
  20. // 设置value的序列化规则和 key的序列化规则
  21. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  22. redisTemplate.setKeySerializer(new StringRedisSerializer());
  23.  
  24. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  25. redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
  26.  
  27. redisTemplate.afterPropertiesSet();
  28. return redisTemplate;
  29. }
  30.  
  31. }
  32.  
  33. class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> {
  34. @Override
  35. public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  36. jsonGenerator.writeString(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
  37. }
  38. }
  39.  
  40. class JodaDateTimeJsonDeserializer extends JsonDeserializer<DateTime> {
  41. @Override
  42. public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
  43. String dateString = jsonParser.readValueAs(String.class);
  44. DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
  45. return dateTimeFormatter.parseDateTime(dateString);
  46. }
  47. }

4.5 常量管理

由于代码中会用到一些共有的常量,我们应该将其抽离出来。

  1. public class LotteryConstants {
  2.  
  3. /**
  4. * 表示正在抽奖的用户标记
  5. */
  6. public final static String DRAWING = "DRAWING";
  7. /**
  8. * 活动标记 LOTTERY:lotteryID
  9. */
  10. public final static String LOTTERY = "LOTTERY";
  11. /**
  12. * 奖品数据 LOTTERY_PRIZE:lotteryID:PrizeId
  13. */
  14. public final static String LOTTERY_PRIZE = "LOTTERY_PRIZE";
  15. /**
  16. * 默认奖品数据 DEFAULT_LOTTERY_PRIZE:lotteryID
  17. */
  18. public final static String DEFAULT_LOTTERY_PRIZE = "DEFAULT_LOTTERY_PRIZE";
  19.  
  20. public enum PrizeTypeEnum {
  21. THANK(-1), NORMAL(1), UNIQUE(2);
  22. private int value;
  23.  
  24. private PrizeTypeEnum(int value) {
  25. this.value = value;
  26. }
  27.  
  28. public int getValue() {
  29. return this.value;
  30. }
  31. }
  32.  
  33. /**
  34. * 奖项缓存:LOTTERY_ITEM:LOTTERY_ID
  35. */
  36. public final static String LOTTERY_ITEM = "LOTTERY_ITEM";
  37. /**
  38. * 默认奖项: DEFAULT_LOTTERY_ITEM:LOTTERY_ID
  39. */
  40. public final static String DEFAULT_LOTTERY_ITEM = "DEFAULT_LOTTERY_ITEM";
  41.  
  42. }
  1. public enum ReturnCodeEnum {
  2.  
  3. SUCCESS("0000", "成功"),
  4.  
  5. LOTTER_NOT_EXIST("9001", "指定抽奖活动不存在"),
  6.  
  7. LOTTER_FINISH("9002", "活动已结束"),
  8.  
  9. LOTTER_REPO_NOT_ENOUGHT("9003", "当前奖品库存不足"),
  10.  
  11. LOTTER_ITEM_NOT_INITIAL("9004", "奖项数据未初始化"),
  12.  
  13. LOTTER_DRAWING("9005", "上一次抽奖还未结束"),
  14.  
  15. REQUEST_PARAM_NOT_VALID("9998", "请求参数不正确"),
  16.  
  17. SYSTEM_ERROR("9999", "系统繁忙,请稍后重试");
  18.  
  19. private String code;
  20.  
  21. private String msg;
  22.  
  23. private ReturnCodeEnum(String code, String msg) {
  24. this.code = code;
  25. this.msg = msg;
  26. }
  27.  
  28. public String getCode() {
  29. return code;
  30. }
  31.  
  32. public String getMsg() {
  33. return msg;
  34. }
  35.  
  36. public String getCodeString() {
  37. return getCode() + "";
  38. }
  39. }

对Redis中的key进行统一的管理。

  1. public class RedisKeyManager {
  2.  
  3. /**
  4. * 正在抽奖的key
  5. *
  6. * @param accountIp
  7. * @return
  8. */
  9. public static String getDrawingRedisKey(String accountIp) {
  10. return new StringBuilder(LotteryConstants.DRAWING).append(":").append(accountIp).toString();
  11. }
  12.  
  13. /**
  14. * 获取抽奖活动的key
  15. *
  16. * @param id
  17. * @return
  18. */
  19. public static String getLotteryRedisKey(Integer id) {
  20. return new StringBuilder(LotteryConstants.LOTTERY).append(":").append(id).toString();
  21. }
  22.  
  23. /**
  24. * 获取指定活动下的所有奖品数据
  25. *
  26. * @param lotteryId
  27. * @return
  28. */
  29. public static String getLotteryPrizeRedisKey(Integer lotteryId) {
  30. return new StringBuilder(LotteryConstants.LOTTERY_PRIZE).append(":").append(lotteryId).toString();
  31. }
  32.  
  33. public static String getLotteryPrizeRedisKey(Integer lotteryId, Integer prizeId) {
  34. return new StringBuilder(LotteryConstants.LOTTERY_PRIZE).append(":").append(lotteryId).append(":").append(prizeId).toString();
  35. }
  36.  
  37. public static String getDefaultLotteryPrizeRedisKey(Integer lotteryId) {
  38. return new StringBuilder(LotteryConstants.DEFAULT_LOTTERY_PRIZE).append(":").append(lotteryId).toString();
  39. }
  40.  
  41. public static String getLotteryItemRedisKey(Integer lotteryId) {
  42. return new StringBuilder(LotteryConstants.LOTTERY_ITEM).append(":").append(lotteryId).toString();
  43. }
  44.  
  45. public static String getDefaultLotteryItemRedisKey(Integer lotteryId) {
  46. return new StringBuilder(LotteryConstants.DEFAULT_LOTTERY_ITEM).append(":").append(lotteryId).toString();
  47. }
  48. }

4.6 业务代码

4.6.1 抽奖接口

我们首先编写抽奖接口,根据前台传的参数查询到具体的活动,然后进行相应的操作。(当然,前端直接是写死的/lottery/1)

  1. @GetMapping("/{id}")
  2. public ResultResp<LotteryItemVo> doDraw(@PathVariable("id") Integer id, HttpServletRequest request) {
  3. String accountIp = CusAccessObjectUtil.getIpAddress(request);
  4. log.info("begin LotteryController.doDraw,access user {}, lotteryId,{}:", accountIp, id);
  5. ResultResp<LotteryItemVo> resultResp = new ResultResp<>();
  6. try {
  7. //判断当前用户上一次抽奖是否结束
  8. checkDrawParams(id, accountIp);
  9.  
  10. //抽奖
  11. DoDrawDto dto = new DoDrawDto();
  12. dto.setAccountIp(accountIp);
  13. dto.setLotteryId(id);
  14. lotteryService.doDraw(dto);
  15.  
  16. //返回结果设置
  17. resultResp.setCode(ReturnCodeEnum.SUCCESS.getCode());
  18. resultResp.setMsg(ReturnCodeEnum.SUCCESS.getMsg());
  19. //对象转换
  20. resultResp.setResult(lotteryConverter.dto2LotteryItemVo(dto));
  21. } catch (Exception e) {
  22. return ExceptionUtil.handlerException4biz(resultResp, e);
  23. } finally {
  24. //清除占位标记
  25. redisTemplate.delete(RedisKeyManager.getDrawingRedisKey(accountIp));
  26. }
  27. return resultResp;
  28. }
  29.  
  30. private void checkDrawParams(Integer id, String accountIp) {
  31. if (null == id) {
  32. throw new RewardException(ReturnCodeEnum.REQUEST_PARAM_NOT_VALID.getCode(), ReturnCodeEnum.REQUEST_PARAM_NOT_VALID.getMsg());
  33. }
  34. //采用setNx命令,判断当前用户上一次抽奖是否结束
  35. Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getDrawingRedisKey(accountIp), "1", 60, TimeUnit.SECONDS);
  36. //如果为false,说明上一次抽奖还未结束
  37. if (!result) {
  38. throw new RewardException(ReturnCodeEnum.LOTTER_DRAWING.getCode(), ReturnCodeEnum.LOTTER_DRAWING.getMsg());
  39. }
  40. }

为了避免用户重复点击抽奖,所以我们通过Redis来避免这种问题,用户每次抽奖的时候,通过setNx给用户排队并设置过期时间;如果用户点击多次抽奖,Redis设置值的时候发现该用户上次抽奖还未结束则抛出异常。

最后用户抽奖成功的话,记得清除该标记,从而用户能够继续抽奖。

4.6.2 初始化数据

从抽奖入口进来,校验成功以后则开始业务操作。

  1. @Override
  2. public void doDraw(DoDrawDto drawDto) throws Exception {
  3. RewardContext context = new RewardContext();
  4. LotteryItem lotteryItem = null;
  5. try {
  6. //JUC工具 需要等待线程结束之后才能运行
  7. CountDownLatch countDownLatch = new CountDownLatch(1);
  8. //判断活动有效性
  9. Lottery lottery = checkLottery(drawDto);
  10. //发布事件,用来加载指定活动的奖品信息
  11. applicationContext.publishEvent(new InitPrizeToRedisEvent(this, lottery.getId(), countDownLatch));
  12. //开始抽奖
  13. lotteryItem = doPlay(lottery);
  14. //记录奖品并扣减库存
  15. countDownLatch.await(); //等待奖品初始化完成
  16. String key = RedisKeyManager.getLotteryPrizeRedisKey(lottery.getId(), lotteryItem.getPrizeId());
  17. int prizeType = Integer.parseInt(redisTemplate.opsForHash().get(key, "prizeType").toString());
  18. context.setLottery(lottery);
  19. context.setLotteryItem(lotteryItem);
  20. context.setAccountIp(drawDto.getAccountIp());
  21. context.setKey(key);
  22. //调整库存及记录中奖信息
  23. AbstractRewardProcessor.rewardProcessorMap.get(prizeType).doReward(context);
  24. } catch (UnRewardException u) { //表示因为某些问题未中奖,返回一个默认奖项
  25. context.setKey(RedisKeyManager.getDefaultLotteryPrizeRedisKey(lotteryItem.getLotteryId()));
  26. lotteryItem = (LotteryItem) redisTemplate.opsForValue().get(RedisKeyManager.getDefaultLotteryItemRedisKey(lotteryItem.getLotteryId()));
  27. context.setLotteryItem(lotteryItem);
  28. AbstractRewardProcessor.rewardProcessorMap.get(LotteryConstants.PrizeTypeEnum.THANK.getValue()).doReward(context);
  29. }
  30. //拼接返回数据
  31. drawDto.setLevel(lotteryItem.getLevel());
  32. drawDto.setPrizeName(context.getPrizeName());
  33. drawDto.setPrizeId(context.getPrizeId());
  34. }

首先我们通过CountDownLatch来保证商品初始化的顺序,关于CountDownLatch可以查看 JUC工具 该文章。

然后我们需要检验一下活动的有效性,确保活动未结束。

检验活动通过后则通过ApplicationEvent 事件实现奖品数据的加载,将其存入Redis中。或者通过ApplicationRunner在程序启动时获取相关数据。我们这使用的是事件机制。ApplicationRunner 的相关代码在下文我也顺便贴出。

事件机制

  1. public class InitPrizeToRedisEvent extends ApplicationEvent {
  2.  
  3. private Integer lotteryId;
  4.  
  5. private CountDownLatch countDownLatch;
  6.  
  7. public InitPrizeToRedisEvent(Object source, Integer lotteryId, CountDownLatch countDownLatch) {
  8. super(source);
  9. this.lotteryId = lotteryId;
  10. this.countDownLatch = countDownLatch;
  11. }
  12.  
  13. public Integer getLotteryId() {
  14. return lotteryId;
  15. }
  16.  
  17. public void setLotteryId(Integer lotteryId) {
  18. this.lotteryId = lotteryId;
  19. }
  20.  
  21. public CountDownLatch getCountDownLatch() {
  22. return countDownLatch;
  23. }
  24.  
  25. public void setCountDownLatch(CountDownLatch countDownLatch) {
  26. this.countDownLatch = countDownLatch;
  27. }
  28. }

有了事件机制,我们还需要一个监听事件,用来初始化相关数据信息。具体业务逻辑大家可以参考下代码,有相关的注释信息,主要就是将数据库中的数据添加进redis中,需要注意的是,我们为了保证原子性,是通过HASH来存储数据的,这样之后库存扣减的时候就可以通过opsForHash来保证其原子性。

当初始化奖品信息之后,则通过countDown()方法表名执行完成,业务代码中线程阻塞的地方可以继续执行了。

  1. @Slf4j
  2. @Component
  3. public class InitPrizeToRedisListener implements ApplicationListener<InitPrizeToRedisEvent> {
  4.  
  5. @Autowired
  6. RedisTemplate redisTemplate;
  7.  
  8. @Autowired
  9. LotteryPrizeMapper lotteryPrizeMapper;
  10.  
  11. @Autowired
  12. LotteryItemMapper lotteryItemMapper;
  13.  
  14. @Override
  15. public void onApplicationEvent(InitPrizeToRedisEvent initPrizeToRedisEvent) {
  16. log.info("begin InitPrizeToRedisListener," + initPrizeToRedisEvent);
  17. Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId()), "1");
  18. //已经初始化到缓存中了,不需要再次缓存
  19. if (!result) {
  20. log.info("already initial");
  21. initPrizeToRedisEvent.getCountDownLatch().countDown();
  22. return;
  23. }
  24. QueryWrapper<LotteryItem> lotteryItemQueryWrapper = new QueryWrapper<>();
  25. lotteryItemQueryWrapper.eq("lottery_id", initPrizeToRedisEvent.getLotteryId());
  26. List<LotteryItem> lotteryItems = lotteryItemMapper.selectList(lotteryItemQueryWrapper);
  27.  
  28. //如果指定的奖品没有了,会生成一个默认的奖项
  29. LotteryItem defaultLotteryItem = lotteryItems.parallelStream().filter(o -> o.getDefaultItem().intValue() == 1).findFirst().orElse(null);
  30.  
  31. Map<String, Object> lotteryItemMap = new HashMap<>(16);
  32. lotteryItemMap.put(RedisKeyManager.getLotteryItemRedisKey(initPrizeToRedisEvent.getLotteryId()), lotteryItems);
  33. lotteryItemMap.put(RedisKeyManager.getDefaultLotteryItemRedisKey(initPrizeToRedisEvent.getLotteryId()), defaultLotteryItem);
  34. redisTemplate.opsForValue().multiSet(lotteryItemMap);
  35.  
  36. QueryWrapper queryWrapper = new QueryWrapper();
  37. queryWrapper.eq("lottery_id", initPrizeToRedisEvent.getLotteryId());
  38. List<LotteryPrize> lotteryPrizes = lotteryPrizeMapper.selectList(queryWrapper);
  39.  
  40. //保存一个默认奖项
  41. AtomicReference<LotteryPrize> defaultPrize = new AtomicReference<>();
  42. lotteryPrizes.stream().forEach(lotteryPrize -> {
  43. if (lotteryPrize.getId().equals(defaultLotteryItem.getPrizeId())) {
  44. defaultPrize.set(lotteryPrize);
  45. }
  46. String key = RedisKeyManager.getLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId(), lotteryPrize.getId());
  47. setLotteryPrizeToRedis(key, lotteryPrize);
  48. });
  49. String key = RedisKeyManager.getDefaultLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId());
  50. setLotteryPrizeToRedis(key, defaultPrize.get());
  51. initPrizeToRedisEvent.getCountDownLatch().countDown(); //表示初始化完成
  52. log.info("finish InitPrizeToRedisListener," + initPrizeToRedisEvent);
  53. }
  54.  
  55. private void setLotteryPrizeToRedis(String key, LotteryPrize lotteryPrize) {
  56. redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
  57. redisTemplate.opsForHash().put(key, "id", lotteryPrize.getId());
  58. redisTemplate.opsForHash().put(key, "lotteryId", lotteryPrize.getLotteryId());
  59. redisTemplate.opsForHash().put(key, "prizeName", lotteryPrize.getPrizeName());
  60. redisTemplate.opsForHash().put(key, "prizeType", lotteryPrize.getPrizeType());
  61. redisTemplate.opsForHash().put(key, "totalStock", lotteryPrize.getTotalStock());
  62. redisTemplate.opsForHash().put(key, "validStock", lotteryPrize.getValidStock());
  63. }
  64. }

上面部分是通过事件的方法来初始化数据,下面我们说下ApplicationRunner的方式:

这种方式很简单,在项目启动的时候将数据加载进去即可。

我们只需要实现ApplicationRunner接口即可,然后在run方法中从数据库读取数据加载到Redis中。

  1. @Slf4j
  2. @Component
  3. public class LoadDataApplicationRunner implements ApplicationRunner {
  4.  
  5.  
  6. @Autowired
  7. RedisTemplate redisTemplate;
  8.  
  9. @Autowired
  10. LotteryMapper lotteryMapper;
  11.  
  12. @Override
  13. public void run(ApplicationArguments args) throws Exception {
  14. log.info("=========begin load lottery data to Redis===========");
  15. //加载当前抽奖活动信息
  16. Lottery lottery = lotteryMapper.selectById(1);
  17.  
  18. log.info("=========finish load lottery data to Redis===========");
  19. }
  20. }

4.6.3 抽奖

我们在使用事件进行数据初始化的时候,可以同时进行抽奖操作,但是注意的是这个时候需要使用countDownLatch.await();来阻塞当前线程,等待数据初始化完成。

在抽奖的过程中,我们首先尝试从Redis中获取相关数据,如果Redis中没有则从数据库中加载数据,如果数据库中也没查询到相关数据,则表明相关的数据没有配置完成。

获取数据之后,我们就该开始抽奖了。抽奖的核心在于随机性以及概率性,咱们总不能随便抽抽都能抽到一等奖吧?所以我们需要在表中设置每个奖项的概率性。如下所示:

image-20211229212035238

在我们抽奖的时候需要根据概率划分处相关区间。我们可以通过Debug的方式来查看一下具体怎么划分的:

奖项的概率越大,区间越大;大家看到的顺序是不同的,由于我们在上面通过Collections.shuffle(lotteryItems);将集合打乱了,所以这里看到的不是顺序展示的。

image-20211229212634205

在生成对应区间后,我们通过生成随机数,看随机数落在那个区间中,然后将对应的奖项返回。这就实现了我们的抽奖过程。

  1. private LotteryItem doPlay(Lottery lottery) {
  2. LotteryItem lotteryItem = null;
  3. QueryWrapper<LotteryItem> queryWrapper = new QueryWrapper<>();
  4. queryWrapper.eq("lottery_id", lottery.getId());
  5. Object lotteryItemsObj = redisTemplate.opsForValue().get(RedisKeyManager.getLotteryItemRedisKey(lottery.getId()));
  6. List<LotteryItem> lotteryItems;
  7. //说明还未加载到缓存中,同步从数据库加载,并且异步将数据缓存
  8. if (lotteryItemsObj == null) {
  9. lotteryItems = lotteryItemMapper.selectList(queryWrapper);
  10. } else {
  11. lotteryItems = (List<LotteryItem>) lotteryItemsObj;
  12. }
  13. //奖项数据未配置
  14. if (lotteryItems.isEmpty()) {
  15. throw new BizException(ReturnCodeEnum.LOTTER_ITEM_NOT_INITIAL.getCode(), ReturnCodeEnum.LOTTER_ITEM_NOT_INITIAL.getMsg());
  16. }
  17. int lastScope = 0;
  18. Collections.shuffle(lotteryItems);
  19. Map<Integer, int[]> awardItemScope = new HashMap<>();
  20. //item.getPercent=0.05 = 5%
  21. for (LotteryItem item : lotteryItems) {
  22. int currentScope = lastScope + new BigDecimal(item.getPercent().floatValue()).multiply(new BigDecimal(mulriple)).intValue();
  23. awardItemScope.put(item.getId(), new int[]{lastScope + 1, currentScope});
  24. lastScope = currentScope;
  25. }
  26. int luckyNumber = new Random().nextInt(mulriple);
  27. int luckyPrizeId = 0;
  28. if (!awardItemScope.isEmpty()) {
  29. Set<Map.Entry<Integer, int[]>> set = awardItemScope.entrySet();
  30. for (Map.Entry<Integer, int[]> entry : set) {
  31. if (luckyNumber >= entry.getValue()[0] && luckyNumber <= entry.getValue()[1]) {
  32. luckyPrizeId = entry.getKey();
  33. break;
  34. }
  35. }
  36. }
  37. for (LotteryItem item : lotteryItems) {
  38. if (item.getId().intValue() == luckyPrizeId) {
  39. lotteryItem = item;
  40. break;
  41. }
  42. }
  43. return lotteryItem;
  44. }

4.6.4 调整库存及记录

在调整库存的时候,我们需要考虑到每个奖品类型的不同,根据不同类型的奖品采取不同的措施。比如如果是一些价值高昂的奖品,我们需要通过分布式锁来确保安全性;或者比如有些商品我们需要发送相应的短信;所以我们需要采取一种具有扩展性的实现机制。

具体的实现机制可以看下方的类图,我首先定义一个奖品方法的接口(RewardProcessor),然后定义一个抽象类(AbstractRewardProcessor),抽象类中定义了模板方法,然后我们就可以根据不同的类型创建不同的处理器即可,这大大加强了我们的扩展性。

比如我们这边就创建了库存充足处理器及库存不足处理器。

image-20211229214246943

image-20211229214223549

接口:

  1. public interface RewardProcessor<T> {
  2.  
  3. void doReward(RewardContext context);
  4.  
  5. }

抽象类:

  1. @Slf4j
  2. public abstract class AbstractRewardProcessor implements RewardProcessor<RewardContext>, ApplicationContextAware {
  3.  
  4. public static Map<Integer, RewardProcessor> rewardProcessorMap = new ConcurrentHashMap<Integer, RewardProcessor>();
  5.  
  6. @Autowired
  7. protected RedisTemplate redisTemplate;
  8.  
  9. private void beforeProcessor(RewardContext context) {
  10. }
  11.  
  12. @Override
  13. public void doReward(RewardContext context) {
  14. beforeProcessor(context);
  15. processor(context);
  16. afterProcessor(context);
  17. }
  18.  
  19. protected abstract void afterProcessor(RewardContext context);
  20.  
  21.  
  22. /**
  23. * 发放对应的奖品
  24. *
  25. * @param context
  26. */
  27. protected abstract void processor(RewardContext context);
  28.  
  29. /**
  30. * 返回当前奖品类型
  31. *
  32. * @return
  33. */
  34. protected abstract int getAwardType();
  35.  
  36. @Override
  37. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  38. rewardProcessorMap.put(LotteryConstants.PrizeTypeEnum.THANK.getValue(), (RewardProcessor) applicationContext.getBean(NoneStockRewardProcessor.class));
  39. rewardProcessorMap.put(LotteryConstants.PrizeTypeEnum.NORMAL.getValue(), (RewardProcessor) applicationContext.getBean(HasStockRewardProcessor.class));
  40. }
  41. }

我们可以从抽象类中的doReward方法处开始查看,比如我们这边先查看库存充足处理器中的代码:

库存处理器执行的时候首相将Redis中对应的奖项库存减1,这时候是不需要加锁的,因为这个操作是原子性的。

当扣减后,我们根据返回的值判断商品库存是否充足,这个时候库存不足则提示未中奖或者返回一个默认商品。

最后我们还需要记得更新下数据库中的相关数据。

  1. @Override
  2. protected void processor(RewardContext context) {
  3. //扣减库存(redis的更新)
  4. Long result = redisTemplate.opsForHash().increment(context.getKey(), "validStock", -1);
  5. //当前奖品库存不足,提示未中奖,或者返回一个兜底的奖品
  6. if (result.intValue() < 0) {
  7. throw new UnRewardException(ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getCode(), ReturnCodeEnum.LOTTER_REPO_NOT_ENOUGHT.getMsg());
  8. }
  9. List<Object> propertys = Arrays.asList("id", "prizeName");
  10. List<Object> prizes = redisTemplate.opsForHash().multiGet(context.getKey(), propertys);
  11. context.setPrizeId(Integer.parseInt(prizes.get(0).toString()));
  12. context.setPrizeName(prizes.get(1).toString());
  13. //更新库存(数据库的更新)
  14. lotteryPrizeMapper.updateValidStock(context.getPrizeId());
  15. }

方法执行完成之后,我们需要执行afterProcessor方法:

这个地方我们是通过异步任务异步存入抽奖记录信息。

  1. @Override
  2. protected void afterProcessor(RewardContext context) {
  3. asyncLotteryRecordTask.saveLotteryRecord(context.getAccountIp(), context.getLotteryItem(), context.getPrizeName());
  4. }

在这边我们可以发现是通过Async注解,指定一个线程池,开启一个异步执行的方法。

  1. @Slf4j
  2. @Component
  3. public class AsyncLotteryRecordTask {
  4.  
  5. @Autowired
  6. LotteryRecordMapper lotteryRecordMapper;
  7.  
  8. @Async("lotteryServiceExecutor")
  9. public void saveLotteryRecord(String accountIp, LotteryItem lotteryItem, String prizeName) {
  10. log.info(Thread.currentThread().getName() + "---saveLotteryRecord");
  11. //存储中奖信息
  12. LotteryRecord record = new LotteryRecord();
  13. record.setAccountIp(accountIp);
  14. record.setItemId(lotteryItem.getId());
  15. record.setPrizeName(prizeName);
  16. record.setCreateTime(LocalDateTime.now());
  17. lotteryRecordMapper.insert(record);
  18. }
  19. }

创建一个线程池:相关的配置信息是我们定义在YML文件中的数据。

  1. @Configuration
  2. @EnableAsync
  3. @EnableConfigurationProperties(ThreadPoolExecutorProperties.class)
  4. public class ThreadPoolExecutorConfig {
  5.  
  6. @Bean(name = "lotteryServiceExecutor")
  7. public Executor lotteryServiceExecutor(ThreadPoolExecutorProperties poolExecutorProperties) {
  8. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  9. executor.setCorePoolSize(poolExecutorProperties.getCorePoolSize());
  10. executor.setMaxPoolSize(poolExecutorProperties.getMaxPoolSize());
  11. executor.setQueueCapacity(poolExecutorProperties.getQueueCapacity());
  12. executor.setThreadNamePrefix(poolExecutorProperties.getNamePrefix());
  13. executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
  14. return executor;
  15. }
  16. }
  1. @Data
  2. @ConfigurationProperties(prefix = "async.executor.thread")
  3. public class ThreadPoolExecutorProperties {
  4. private int corePoolSize;
  5. private int maxPoolSize;
  6. private int queueCapacity;
  7. private String namePrefix;
  8. }

4.7 总结

以上便是整个项目的搭建,关于前端界面无非就是向后端发起请求,根据返回的奖品信息,将指针落在对应的转盘位置处,具体代码可以前往项目地址查看。希望大家可以动个小手点点赞,嘻嘻。

5. 项目地址

如果直接使用项目的话,记得修改数据库中活动的结束时间。

Redis

具体的实战项目在lottery工程中。

image-20211229221247136

到此这篇关于Redis 抽奖大转盘的实战示例的文章就介绍到这了,更多相关Redis 抽奖大转盘内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号