Redis?抽獎(jiǎng)大轉(zhuǎn)盤的實(shí)戰(zhàn)示例
1. 項(xiàng)目介紹
這是一個(gè)基于Spring boot + Mybatis Plus + Redis 的簡(jiǎn)單案例。
主要是將活動(dòng)內(nèi)容、獎(jiǎng)品信息、記錄信息等緩存到Redis中,然后所有的抽獎(jiǎng)過(guò)程全部從Redis中做數(shù)據(jù)的操作。
大致內(nèi)容很簡(jiǎn)單,具體操作下面慢慢分析。
2. 項(xiàng)目演示
話不多說(shuō),首先上圖看看項(xiàng)目效果,如果覺(jué)得還行的話咱們就來(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等依賴。
<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配置
依賴引入之后,我們需要進(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<Object, Object> 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<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 常量管理
由于代碼中會(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<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 {
//判斷當(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),所以我們通過(guò)Redis來(lái)避免這種問(wèn)題,用戶每次抽獎(jiǎng)的時(shí)候,通過(guò)setNx給用戶排隊(duì)并設(shè)置過(guò)期時(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)成功以后則開(kāi)始業(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));
//開(kāi)始抽獎(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)槟承﹩?wè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());
}
首先我們通過(guò)CountDownLatch來(lái)保證商品初始化的順序,關(guān)于CountDownLatch可以查看 JUC工具 該文章。
然后我們需要檢驗(yàn)一下活動(dòng)的有效性,確保活動(dòng)未結(jié)束。
檢驗(yàn)活動(dòng)通過(guò)后則通過(guò)ApplicationEvent 事件實(shí)現(xiàn)獎(jiǎng)品數(shù)據(jù)的加載,將其存入Redis中?;蛘咄ㄟ^(guò)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)聽(tīng)事件,用來(lái)初始化相關(guān)數(shù)據(jù)信息。具體業(yè)務(wù)邏輯大家可以參考下代碼,有相關(guān)的注釋信息,主要就是將數(shù)據(jù)庫(kù)中的數(shù)據(jù)添加進(jìn)redis中,需要注意的是,我們?yōu)榱吮WC原子性,是通過(guò)HASH來(lái)存儲(chǔ)數(shù)據(jù)的,這樣之后庫(kù)存扣減的時(shí)候就可以通過(guò)opsForHash來(lái)保證其原子性。
當(dāng)初始化獎(jiǎng)品信息之后,則通過(guò)countDown()方法表名執(zhí)行完成,業(yè)務(wù)代碼中線程阻塞的地方可以繼續(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);
//如果指定的獎(jiǎng)品沒(méi)有了,會(huì)生成一個(gè)默認(rèn)的獎(jiǎng)項(xiàng)
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);
//保存一個(gè)默認(rèn)獎(jiǎng)項(xiàng)
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());
}
}
上面部分是通過(guò)事件的方法來(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)的過(guò)程中,我們首先嘗試從Redis中獲取相關(guān)數(shù)據(jù),如果Redis中沒(méi)有則從數(shù)據(jù)庫(kù)中加載數(shù)據(jù),如果數(shù)據(jù)庫(kù)中也沒(méi)查詢到相關(guān)數(shù)據(jù),則表明相關(guān)的數(shù)據(jù)沒(méi)有配置完成。
獲取數(shù)據(jù)之后,我們就該開(kāi)始抽獎(jiǎng)了。抽獎(jiǎng)的核心在于隨機(jī)性以及概率性,咱們總不能隨便抽抽都能抽到一等獎(jiǎng)吧?所以我們需要在表中設(shè)置每個(gè)獎(jiǎng)項(xiàng)的概率性。如下所示:

在我們抽獎(jiǎng)的時(shí)候需要根據(jù)概率劃分處相關(guān)區(qū)間。我們可以通過(guò)Debug的方式來(lái)查看一下具體怎么劃分的:
獎(jiǎng)項(xiàng)的概率越大,區(qū)間越大;大家看到的順序是不同的,由于我們?cè)谏厦嫱ㄟ^(guò)Collections.shuffle(lotteryItems);將集合打亂了,所以這里看到的不是順序展示的。

在生成對(duì)應(yīng)區(qū)間后,我們通過(guò)生成隨機(jī)數(shù),看隨機(jī)數(shù)落在那個(gè)區(qū)間中,然后將對(duì)應(yīng)的獎(jiǎng)項(xiàng)返回。這就實(shí)現(xiàn)了我們的抽獎(jiǎng)過(guò)程。
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;
//說(shuō)明還未加載到緩存中,同步從數(shù)據(jù)庫(kù)加載,并且異步將數(shù)據(jù)緩存
if (lotteryItemsObj == null) {
lotteryItems = lotteryItemMapper.selectList(queryWrapper);
} else {
lotteryItems = (List<LotteryItem>) 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<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 調(diào)整庫(kù)存及記錄
在調(diào)整庫(kù)存的時(shí)候,我們需要考慮到每個(gè)獎(jiǎng)品類型的不同,根據(jù)不同類型的獎(jiǎng)品采取不同的措施。比如如果是一些價(jià)值高昂的獎(jiǎng)品,我們需要通過(guò)分布式鎖來(lái)確保安全性;或者比如有些商品我們需要發(fā)送相應(yīng)的短信;所以我們需要采取一種具有擴(kuò)展性的實(shí)現(xiàn)機(jī)制。
具體的實(shí)現(xiàn)機(jī)制可以看下方的類圖,我首先定義一個(gè)獎(jiǎng)品方法的接口(RewardProcessor),然后定義一個(gè)抽象類(AbstractRewardProcessor),抽象類中定義了模板方法,然后我們就可以根據(jù)不同的類型創(chuàng)建不同的處理器即可,這大大加強(qiáng)了我們的擴(kuò)展性。
比如我們這邊就創(chuàng)建了庫(kù)存充足處理器及庫(kù)存不足處理器。


接口:
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ā)放對(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āi)始查看,比如我們這邊先查看庫(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() < 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());
//更新庫(kù)存(數(shù)據(jù)庫(kù)的更新)
lotteryPrizeMapper.updateValidStock(context.getPrizeId());
}
方法執(zhí)行完成之后,我們需要執(zhí)行afterProcessor方法:
這個(gè)地方我們是通過(guò)異步任務(wù)異步存入抽獎(jiǎng)記錄信息。
@Override
protected void afterProcessor(RewardContext context) {
asyncLotteryRecordTask.saveLotteryRecord(context.getAccountIp(), context.getLotteryItem(), context.getPrizeName());
}
在這邊我們可以發(fā)現(xiàn)是通過(guò)Async注解,指定一個(gè)線程池,開(kāi)啟一個(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)目地址
如果直接使用項(xiàng)目的話,記得修改數(shù)據(jù)庫(kù)中活動(dòng)的結(jié)束時(shí)間。
具體的實(shí)戰(zhàn)項(xiàng)目在lottery工程中。

到此這篇關(guān)于Redis 抽獎(jiǎng)大轉(zhuǎn)盤的實(shí)戰(zhàn)示例的文章就介紹到這了,更多相關(guān)Redis 抽獎(jiǎng)大轉(zhuǎn)盤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Redis中的內(nèi)存淘汰策略和過(guò)期鍵刪除策略
本文主要介紹了淺談Redis中的內(nèi)存淘汰策略和過(guò)期鍵刪除策略,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
淺談一下Redis的數(shù)據(jù)結(jié)構(gòu)
這篇文章主要介紹了淺談一下Redis的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)單字符串結(jié)構(gòu)被用于存儲(chǔ)redis的key對(duì)象和String類型的value對(duì)象,其中的free和len字段可以輕松的使得在該字符串被修改時(shí)判斷是否需要擴(kuò)容,需要的朋友可以參考下2023-08-08

