情景分析:
商家时常会搞一些抽奖活动,这类活动有个特点就是抽奖用户会在抽奖时间突然大量的涌入系统,这时DB瞬间承受压力倍增,随时可能出现宕机的情况,从而影响整个业务。
需求分析:
这类活动通常有以下几个需求:
- 同一用户最多只能抽到一个奖品;
- 若有多轮抽奖,上轮中奖的用户不能再次中奖;
优化思路:
- 既然瓶颈很大一部分是DB导致的,那我们就想办法把请求拦截在上游,尽量减少对DB的读写操作,比如奖品有10个,那只能有10个人中奖,其他人的请求可以直接拦截在业务层,这里采用请求队列来解决,队列大小设置为奖品的数量10,当队列里的请求任务超过10时,直接将后续的抽奖请求返回不中奖;
- 抽奖业务可以采用先到先得的设计思路,即谁的请求先发送过来谁中奖,直到奖品抽完,这样就尽快在短时间内结束抽奖,以减轻对服务器的压力;
- 防止用户连续多次抽奖,若是点击按钮抽奖,可点击一次后按钮置灰,若是摇一摇抽奖,可设置抽奖一次需摇动的时间或者次数多一些;
- 还可采用Nginx来负载均衡,Redis来缓存等;
代码参考:
- 摇一摇抽奖页面:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>摇一摇</title>
<script src="/script/jquery.min.js"></script>
<script type="text/javascript">
//需要判断浏览器是否支持
if (window.DeviceMotionEvent) {
window.addEventListener('devicemotion', deviceMotionHandler, false);
} else{
alert("不支持摇一摇功能");
}
var shake_threshold = 2000;//摇动的阀值
var lastUpdate = 0;//变量保存上次更新的时间
var x, y, z, last_x, last_y, last_z;// x、y、z记录三个轴的数据以及上一次出发的时间
var count = 0;//计数器
var time1;
var time2;
function deviceMotionHandler(eventData) {
if(count==1){
time1 = new Date().getTime();
}
var acceleration = eventData.accelerationIncludingGravity;//获取含重力的加速度
var curTime = new Date().getTime();//获取当前时间
var diffTime = curTime -lastUpdate;//时间差
//固定时间段
if (diffTime > 100) {
lastUpdate = curTime;
x = acceleration.x;
y = acceleration.y;
z = acceleration.z;
var speed = Math.abs(x+y+z-last_x-last_y-last_z) / diffTime * 10000;//速度
//在阀值内
if(speed > shake_threshold){
count++;
//摇动5次都在阀值内开始抽奖
if(count==5){
time2 = new Date().getTime();
$("#form").submit();
return;
}
}
last_x = x;
last_y = y;
last_z = z;
}
}
</script>
<style type="text/css">
.yaodong{
z-index:8;
animation:swing 1s ease 1s 1 both;
-webkit-animation:swing 1s ease 1s infinite both;
-ms-animation:swing 1s ease 1s infinite both;
-o-animation:swing 1s ease 1s infinite both;
-moz-animation:swing 1s ease 1s infinite both;
}
@-webkit-keyframes swing{20%{-webkit-transform:rotate3d(0,0,1,15deg);transform:rotate3d(0,0,1,15deg)}40%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}60%{-webkit-transform:rotate3d(0,0,1,5deg);transform:rotate3d(0,0,1,5deg)}80%{-webkit-transform:rotate3d(0,0,1,-5deg);transform:rotate3d(0,0,1,-5deg)}100%{-webkit-transform:rotate3d(0,0,1,0deg);transform:rotate3d(0,0,1,0deg)}}@keyframes swing{20%{-webkit-transform:rotate3d(0,0,1,15deg);transform:rotate3d(0,0,1,15deg)}40%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}60%{-webkit-transform:rotate3d(0,0,1,5deg);transform:rotate3d(0,0,1,5deg)}80%{-webkit-transform:rotate3d(0,0,1,-5deg);transform:rotate3d(0,0,1,-5deg)}100%{-webkit-transform:rotate3d(0,0,1,0deg);transform:rotate3d(0,0,1,0deg)}}.swing{-webkit-transform-origin:top center;-ms-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}
</style>
</head>
<body>
<form id="form" action="yao_result" method="post">
<input type="hidden" id="flag" name="flag" th:value="${flag}">
<input type="hidden" id="yhbh" name="yhbh" th:value="${session.yhbh}">
</form>
<div style="width:80%;margin:0 auto;margin-top: 25%;">
<img class="yaodong" src="img/sj.png" width="100%"/>
</div>
</body>
</html>
- 后台抽奖逻辑
import java.util.concurrent.BlockingQueue;
import javax.servlet.http.HttpServletRequest;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import cn.com.app.util.ArrayBlockingQueue;
public class Test {
//假设奖品只有10个,则设置队列大小为10
private static final Integer maxQueueSize = 10;
//这里只做入队操作,ArrayBlockingQueue效率比较快,若需要同时做出队操作,则LinkedBlockingQueue效率比较快
private static BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(10);
/**
* 摇一摇
*/
@RequestMapping("/yao")
public String yao(Model model) throws Exception {
return "yao";
}
/**
* 摇奖结果
* @param yhbh:用户唯一编号
*/
@RequestMapping("/yao_result")
public String yaoResult(Model model,HttpServletRequest request,String yhbh) throws Exception {
String result = "no_win";//未中奖页面
//检测当前的队列大小
if (blockingQueue.size() < maxQueueSize) {
//已中奖的用户不可再次中奖
if(!blockingQueue.contains(yhbh) && blockingQueue.offer(yhbh)){
/**
* 这里入库中奖记录
*/
result = "one_win";//中奖页面
}
}
return result;
}
}
- 由于队列中不能包含重复的用户,这里对ArrayBlockingQueue类的入队方法offer源码进行修改,可另保存为工具类:
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
//添加不重复的元素
if(contains(e)){
return false;
}else{
enqueue(e);
return true;
}
}
} finally {
lock.unlock();
}
}