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; }
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):
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进行回滚

最后附上Controller代码:
@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⊙)…),但用之前的乐观锁就不用担心这类情况的发生
