Spring Boot实现抽奖大转盘

2023-10-30 13:10

本文主要是介绍Spring Boot实现抽奖大转盘,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

往期热门文章:
 
 
 
 
 
 
1、Logback这样配置,性能提升10倍!
2、还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!
3、MySQL模糊查询再也用不着 like+% 了!
4、还重构?就这代码只能铲了重写!
5、是怎么样的SQL优化能做到 900W+数据,从17s到300ms?

1、项目介绍

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

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

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

2、项目演示

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

798fdf4f22049ca15c1f196f8ece27f5.png 89900f4906f3a54cfddbfb46d60f3c65.png

3、表结构

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

4b479263d88fdc13cd1d621ab28b1746.png

4、项目搭建

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

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

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

server:port: 8080servlet:context-path: /
spring:datasource:druid:url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverinitial-size: 30max-active: 100min-idle: 10max-wait: 60000time-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000validation-query: SELECT 1 FROM DUALtest-while-idle: truetest-on-borrow: falsetest-on-return: falsefilters: stat,wallredis:port: 6379host: 127.0.0.1lettuce:pool:max-active: -1max-idle: 2000max-wait: -1min-idle: 1time-between-eviction-runs: 5000mvc:view:prefix: classpath:/templates/suffix: .html
# mybatis-plus
mybatis-plus:configuration:map-underscore-to-camel-case: trueauto-mapping-behavior: fullmapper-locations: classpath*:mapper/**/*Mapper.xml# 线程池
async:executor:thread:core-pool-size: 6max-pool-size: 12queue-capacity: 100000name-prefix: lottery-service-

4.3 代码生成

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

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

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);// 数据源配置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 {@Beanpublic 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的序列化规则和 key的序列化规则redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}}class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> {@Overridepublic 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> {@Overridepublic 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";/*** 奖品数据  LOTTERY_PRIZE:lotteryID:PrizeId*/public final static String LOTTERY_PRIZE = "LOTTERY_PRIZE";/*** 默认奖品数据  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", "奖项数据未初始化"),LOTTER_DRAWING("9005", "上一次抽奖还未结束"),REQUEST_PARAM_NOT_VALID("9998", "请求参数不正确"),SYSTEM_ERROR("9999", "系统繁忙,请稍后重试");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进行统一的管理。

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();}/*** 获取指定活动下的所有奖品数据** @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 业务代码

4.6.1 抽奖接口

我们首先编写抽奖接口,根据前台传的参数查询到具体的活动,然后进行相应的操作。(当然,前端直接是写死的/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设置值的时候发现该用户上次抽奖还未结束则抛出异常。

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

4.6.2 初始化数据

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

@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);//发布事件,用来加载指定活动的奖品信息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);}//拼接返回数据drawDto.setLevel(lotteryItem.getLevel());drawDto.setPrizeName(context.getPrizeName());drawDto.setPrizeId(context.getPrizeId());
}

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

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

检验活动通过后则通过ApplicationEvent 事件实现奖品数据的加载,将其存入Redis中。或者通过ApplicationRunner在程序启动时获取相关数据。我们这使用的是事件机制。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;}}

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

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

@Slf4j
@Component
public class InitPrizeToRedisListener implements ApplicationListener<InitPrizeToRedisEvent> {@AutowiredRedisTemplate redisTemplate;@AutowiredLotteryPrizeMapper lotteryPrizeMapper;@AutowiredLotteryItemMapper lotteryItemMapper;@Overridepublic void onApplicationEvent(InitPrizeToRedisEvent initPrizeToRedisEvent) {log.info("begin InitPrizeToRedisListener," + initPrizeToRedisEvent);Boolean result = redisTemplate.opsForValue().setIfAbsent(RedisKeyManager.getLotteryPrizeRedisKey(initPrizeToRedisEvent.getLotteryId()), "1");//已经初始化到缓存中了,不需要再次缓存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());}
}

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

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

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

@Slf4j
@Component
public class LoadDataApplicationRunner implements ApplicationRunner {@AutowiredRedisTemplate redisTemplate;@AutowiredLotteryMapper lotteryMapper;@Overridepublic 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 抽奖

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

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

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

62ad0dda4114088a46f57721e50b217e.png

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

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

4613b189f60ab4c7b651b2226fe705c6.png

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

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;//说明还未加载到缓存中,同步从数据库加载,并且异步将数据缓存if (lotteryItemsObj == null) {lotteryItems = lotteryItemMapper.selectList(queryWrapper);} else {lotteryItems = (List<LotteryItem>) lotteryItemsObj;}//奖项数据未配置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 调整库存及记录

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

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

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

0a273d1fd4ad0b8b5853dfbc985a033b.png 3c843aaeaa6fbf3f93463f453294b7fe.png

接口:

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>();@Autowiredprotected RedisTemplate redisTemplate;private void beforeProcessor(RewardContext context) {}@Overridepublic void doReward(RewardContext context) {beforeProcessor(context);processor(context);afterProcessor(context);}protected abstract void afterProcessor(RewardContext context);/*** 发放对应的奖品** @param context*/protected abstract void processor(RewardContext context);/*** 返回当前奖品类型** @return*/protected abstract int getAwardType();@Overridepublic 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方法处开始查看,比如我们这边先查看库存充足处理器中的代码:

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

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

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

@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());//更新库存(数据库的更新)lotteryPrizeMapper.updateValidStock(context.getPrizeId());
}

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

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

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

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

@Slf4j
@Component
public class AsyncLotteryRecordTask {@AutowiredLotteryRecordMapper 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);}
}

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

@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 总结

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

5. 项目地址

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

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

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

4d86490d08496e5df83cf956608dd37c.png
往期热门文章:

1、《历史文章分类导读列表!精选优秀博文都在这里了!》

2、再见 BeanUtils!对比 12 种 Bean 自动映射工具,就它性能最拉跨!

3、暴力拒绝白嫖,著名开源项目作者删库跑路,数千个应用程序无限输出乱码

4、两天两夜,1M图片优化到100kb!

5、12 个顶级 Bug 跟踪工具

6、这些 SQL语句真是让我干瞪眼!

7、1个人6种变现途径收入130万美金在2020年

8、“阿里味” PUA 编程语言火上GitHub热榜,标星2.7K!

9、Chrome 再次引入争议 API,遭同行抵制!

10、IDEA高效使用教程,一劳永逸!

bdf004ff1c27b1573e892dc2ee718a5a.png


这篇关于Spring Boot实现抽奖大转盘的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/308129

相关文章

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus