1. 项目介绍
这是一个基于spring boot + mybatis plus + redis 的简单案例。
主要是将活动内容、奖品信息、记录信息等缓存到redis中,然后所有的抽奖过程全部从redis中做数据的操作。
大致内容很简单,具体操作下面慢慢分析。
2. 项目演示
话不多说,首先上图看看项目效果,如果觉得还行的话咱们就来看看他具体是怎么实现的。
3. 表结构
该项目包含以下四张表,分别是活动表、奖项表、奖品表以及中奖记录表。具体的sql会在文末给出。
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: 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的代码生成器帮助我们生成一些基础的业务代码,避免这些重复的体力活。
这边贴出相关代码,直接修改数据库连接信息、相关包名模块名即可。
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 {
@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的序列化规则和 key的序列化规则
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";
/**
* 奖品数据 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> {
@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");
//已经初始化到缓存中了,不需要再次缓存
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 {
@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 抽奖
我们在使用事件进行数据初始化的时候,可以同时进行抽奖操作,但是注意的是这个时候需要使用countdownlatch.await();来阻塞当前线程,等待数据初始化完成。
在抽奖的过程中,我们首先尝试从redis中获取相关数据,如果redis中没有则从数据库中加载数据,如果数据库中也没查询到相关数据,则表明相关的数据没有配置完成。
获取数据之后,我们就该开始抽奖了。抽奖的核心在于随机性以及概率性,咱们总不能随便抽抽都能抽到一等奖吧?所以我们需要在表中设置每个奖项的概率性。如下所示:
在我们抽奖的时候需要根据概率划分处相关区间。我们可以通过debug的方式来查看一下具体怎么划分的:
奖项的概率越大,区间越大;大家看到的顺序是不同的,由于我们在上面通过collections.shuffle(lotteryitems);将集合打乱了,所以这里看到的不是顺序展示的。
在生成对应区间后,我们通过生成随机数,看随机数落在那个区间中,然后将对应的奖项返回。这就实现了我们的抽奖过程。
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),抽象类中定义了模板方法,然后我们就可以根据不同的类型创建不同的处理器即可,这大大加强了我们的扩展性。
比如我们这边就创建了库存充足处理器及库存不足处理器。
接口:
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);
/**
* 发放对应的奖品
*
* @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方法处开始查看,比如我们这边先查看库存充足处理器中的代码:
库存处理器执行的时候首相将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 {
@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);
}
}
创建一个线程池:相关的配置信息是我们定义在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. 项目地址
如果直接使用项目的话,记得修改数据库中活动的结束时间。
redis
具体的实战项目在lottery工程中。
到此这篇关于redis 抽奖大转盘的实战示例的文章就介绍到这了,更多相关redis 抽奖大转盘内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!