SpringMVC 实现抽奖活动(3.优化—乐观锁实现Version2)

SpringMVC 实现抽奖活动(3.优化—乐观锁实现Version2)

流程图:

代码流程图
针对第一次代码review提出的缺陷与不足,重构了抽奖部分的代码并进行了优化,改用性能更高的乐观锁取代for update。(现在回头来看代码的语义真的好了很多)
在这里插入图片描述

核心代码:

@Transactional
public int doLottery(String urs, long gbId) {
if (StringUtils.isEmpty(urs)) {
throw new IllegalArgumentException("urs can not be null");
}
boolean isProperTime = checkTime();
if (!isProperTime) {
return TIME_ILLEGAL;
}
LotteryRecord lotteryRecord = lotteryRecordDao.getUserRecord(urs, gbId);
if (lotteryRecord == null) {
return NO_QUALIFY;
}
if (lotteryRecord.getLotteryResult() != NOT_PARTICIPATED) {
return ALREADY_LOTTERY;
}
boolean hasWon = judgeLottery();
if (hasWon) {
if (lotteryConfigDao.increaseAcquiredAward() > 0) { //乐观锁实现,控制多用户并发,奖品数量不会过度发放
if (lotteryRecordDao.updateAwarded(urs, gbId, GET_AWARD) == 0) { //控制单用户并发抢码行为
throw new RuntimeException();
}
return SUCCESS_LOTTERY;
}
}
lotteryRecordDao.updateAwarded(urs, gbId, NO_AWARD);
return FAIL_LOTTERY;
}

/**
* 确认是否中奖
*/
private boolean judgeLottery() {
double awardRange = lotteryConfigDao.getConfig().getProbability();
return RandomUtils.nextDouble() <= awardRange;
}

/**
* check current time
*/
private boolean checkTime() {
if (lotteryConfigDao.getConfig() == null) {
return false;
}
long startTime = lotteryConfigDao.getConfig().getAwardTime();
long currentTime = System.currentTimeMillis();
return startTime <= currentTime;
}

乐观锁实现

increaseAcquiredAward() :

public long increaseAcquiredAward() {
String sql = "update " + getQuotedTableName()
+ " set acquiredAward = acquiredAward + 1 where acquiredAward < totalAward";
return (long)updateBySql(sql); //返回更新影响到的数据库记录数
}

通过“where acquiredAward < totalAward”实现乐观锁,其本质和加一个version字段类似(即先读取一次取出version,再用where version = 取出的version进行更新),类似于CAS操作(虽然底层的实现方式不一样,但乐观锁的思想都是相同的)

updateAwarded(urs, gbId, NO_AWARD):

/**
* updateAwarded
* @param status GET_AWARD表示中奖,NO_AWARD表示未中奖 {@link LotteryRecord#lotteryResult}
*/
public int updateAwarded(String urs, long gbId, int status) {
if (StringUtils.isEmpty(urs)) {
return 0;
}
if (status != GET_AWARD && status != NO_AWARD) {
throw new IllegalArgumentException("status should be GET_AWARD/NO_AWARD");
}
String sql = "update " + getQuotedTableName()
+ " set lotteryResult = ?, lotteryTime = ? where lotteryResult = 0 and urs = ? and gbId = ?";
return updateBySql(sql, status, new Date().getTime(), urs, gbId);
}

也是用乐观锁实现,防止单用户并发抢码,若果更新记录条数为0(说明更新失败,单用户并发抢码情况出现),则抛出RuntimeException进行回滚

优化后的meta类:

在这里插入图片描述

最后附上Controller代码:

/**
* lottery
*/
@RequestMapping(value = "/lottery", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST)
@ResponseBody
public BusinessResp lottery(HttpServletRequest request,
@RequestParam("gbId") long gbId) {
String username = UrsLoginInterceptor.getCurrentUserName(request);
if (StringUtils.isEmpty(username)) {
return BusinessResp.builder(BusinessCode.NOT_LOGIN);
}
int status;
try {
status = lotteryService.doLottery(username, gbId);
} catch (RuntimeException e) {
return BusinessResp.builder(ALREADY_LOTTERY);
}
if (status == SUCCESS_LOTTERY) {
return BusinessResp.success();
}
return BusinessResp.builder(status);
}

只有最简单的参数校验,其余逻辑均放在service

小结

第一次做很规范化的工程项目,还是很感谢我的导师涛哥以及部门的大佬们,在评审的时候王老师迟哥麦斯都给了非常多的建议,受益匪浅,其实后来补了阿里的《码出高校 阿里巴巴Java开发手册》很多评审里指出的点都在里面有所提及,强烈推荐,一定要看。做完这个项目对SpringMVC的分层有了更具体的认识,同时也深入理解了乐观锁和悲观锁的一些实现。
其实目前写的还有很大优化的空间,比如可以引入redis做缓存加快qps,但考虑到抽奖场景对性能的要求没那么高,所以这边没有加入redis

另一种版本:基于version的乐观锁

另一种写法的乐观锁(其中的userCount相当于version),此处while里加了dao操作,不建议使用,非常耗性能,这个写法原意是,多用户并发场景下,如果一个用户成功中奖了,但没能成功将此结果写入数据库(其他实例已更改过数据库导致userCount失效),那么这时候就用while直到其成功写入数据库。
这个时候前辈的建议是更新失败直接回滚,当它抽奖失败处理((⊙o⊙)…),但用之前的乐观锁就不用担心这类情况的发生
在这里插入图片描述

Author: Apiao
Link: https://Apiao-1.github.io/2018/11/01/2018-11-01/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.