一、背景说明
随着互联网的进步和推广,高并发几乎是所有系统面临的问题,某些接口需要涉及同步线程处理,而这类接口在面临着高并发的请求冲击下,有请求进入排队时,请求是怎么处理的呢?
二、问题案例
springmvc+mybatis框架现在成为时下最流行的后端架构框架,controller接收request请求,调用service的业务处理逻辑,最后返回响应。
设想当高并发请求抢占某些有限资源,则需要在业务处理的逻辑层面通过线程同步synchronized来处理,假设请求1秒钟来一个,而线程同步逻辑处理层需要6秒钟。那么回出现什么情况呢,直接上代码,用结果说明。
spring的所有controller默认都是单例,消费VIP接口在处理逻辑上类似于秒杀,为防止
1.获取request请求controller
@Controller
@RequestMapping("/common")
public class CommonController{
@Autowired
CommonService commonService;
private static int requestNum = 1;//成员变量记录请求序列号(因为spring中所有的controller在容器中默认都是单例singleton模式,该类和成员变量都是JVM中存在唯一一份的代代码片段二进制码)
/**
* 消费VIPCode
*/
@RequestMapping(value="/useVipCode",method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> useVipCode(HttpServletRequest request, HttpServletResponse response){
response.setHeader("Access-Control-Allow-Origin","*");
Map<String, Object> map = new HashMap<String, Object>();
String vipCode = request.getParameter("vipCode");
//调用业务逻辑层
int currentRequestNum = requestNum;
requestNum ++;
return commonService.UseVipCode(vipCode,currentRequestNum);
}
}
}
2.业务处理层service
@Service("commonService")
public class CommonService {
/**
* 使用VIPCode码
*/
public synchronized Map<String,Object> UseVipCode1(String vipCode,int currentRequestNum){//使用线程同步来防止多线程抢占资源
Map<String,Object> map = new HashMap<String, Object>();
//查询VIPCode信息
//根据VIPCode信息计算每一项的到期日期并且开通权限
//调用使用VIPCode的接口
System.out.println("【正在处理请求:"+currentRequestNum+"】VIPCode:"+vipCode);
try {
Thread.currentThread().sleep(6000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
map.put("requestNum", currentRequestNum);
map.put("code", "0000");
map.put("message", "处理成功!");
return map;
}
}
采用多线程执行
public class Test {
public static void main(String[] args) throws Exception {
for(int i=0;i<5;i++){
new Thread(new HttpThread(),"code"+i).start();
Thread.sleep(1000);
}
}
}
class HttpThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"开始启动!");
try {
HttpFunction();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void HttpFunction() throws Exception{
HttpClient client = new HttpClient();
PostMethod method = new PostMethod("http://localhost:8088/DemoTest/common/useVipCode");
try{
method.addParameter("vipCode", Thread.currentThread().getName());
int result = client.executeMethod(method);
if (result == HttpStatus.SC_OK) {
InputStream in = method.getResponseBodyAsStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println(URLDecoder.decode(baos.toString(), "UTF-8"));
} else {
throw new Exception("HTTP ERROR Status: " + method.getStatusCode() + ":" + method.getStatusText());
}
}finally {
method.releaseConnection();
}
}
}
运行结果
code0开始启动!
log4j:WARN No appenders could be found for logger (org.apache.commons.httpclient.HttpClient).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
code1开始启动!
code2开始启动!
code3开始启动!
code4开始启动!
{"requestNum":1,"code":"0000","message":"处理成功!"}
{"requestNum":5,"code":"0000","message":"处理成功!"}
{"requestNum":4,"code":"0000","message":"处理成功!"}
{"requestNum":3,"code":"0000","message":"处理成功!"}
{"requestNum":2,"code":"0000","message":"处理成功!"}
通过结果可以看到,在多线程请求某个同步服务而此项服务需要的时间较长时,会出现后面的请求优先得到相应,而第二个请求反而最后才得到请求。超出我们所想的FIFO的结果,这是怎么回事呢?
三、问题分析
为什么会出现这个现象呢?多线程在我们知道JVM在处理多线程调度中有一个重要的寄存中心,那就是线程栈,当同步线程在处理第一个线程1请求的时候,对其他线程请求上锁,而等待线程2-5则会根据请求顺序进入线程调度栈等待唤醒,待同步线程处理完线程1请求时,再从线程栈里调度新的线程执行该段线程同步代码,故在第6秒时 线程栈里已经根据入栈先后顺序进入的是2-3-4-5四个线程,而根据栈的LIFO规则,所以会调度6秒时最后入栈的线程请求5进行处理,然后依次处理线程4-3-2。
那该怎么解决呢?
很显然首先要求同步线程代码段需要按照请求 顺序来执行,则需要一个全局的队列来接收线程请求,然后同步线程甚至可以跟请求分开,作为一个单独的监听线程专门处理丢到全局队列的请求,但是多线程请求端把请求的request(另外带上当前线程的标识标记)丢到了全局队列之后,等于和处理线程是解耦的异步,那又怎么去等待拿取处理后的结果呢?这时候可以再定义一个全局的缓存,在同步线程在处理完成后将线程标识和处理结果当做一个键值对存入全局缓存中,请求端再来根据传入队列的标识标记在结果缓存中定时轮询获取结果,直到获取到结果后返回response响应。(这里的队列和缓存可以通过直接java里建立全局队列和Map,也可以使用MQ,redis等工具来代替)。
四、部分代码
1.获取request请求controller
@Controller
@RequestMapping("/common")
public class CommonController{
@Autowired
CommonService commonService;
private static int requestNum = 1;//成员变量记录请求序列号(因为spring中所有的controller在容器中默认都是单例singleton模式,该类和成员变量都是JVM中存在唯一一份的代代码片段二进制码)
public static Map<Integer,Object> resultQueue = new HashMap<Integer,Object>();//存放结果集 大型可以使用redis memcached等缓存插件代替
public static BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();//存放请求队列 可使用mq kafka等企业型队列
/**
* 消费VIPCode
*/
@SuppressWarnings("unchecked")
@RequestMapping(value="/useVipCode",method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> useVipCode(HttpServletRequest request, HttpServletResponse response){
response.setHeader("Access-Control-Allow-Origin","*");
Map<String, Object> map = new HashMap<String, Object>();
requestQueue.put(requestNum, request);
int currentNum = requestNum;
requestNum ++;
queue.offer(currentNum);
System.out.println("【接收到请求:"+(currentNum)+"】");
new Thread(new UserThread(commonService)).start();//这里只是触发新的线程等待请求线程同步处理,启用线程解偶,也可让同步放在一个监听线程直接执行请求队列信息,而这里不需要调度
while(true){
map = (Map<String, Object>) resultQueue.get(currentNum);
if(map==null){
continue;
}else{
break;
}
}
return map;
}
}
class UserThread implements Runnable{
private CommonService commonService;
UserThread(CommonService commonService){
this.commonService = commonService;
}
@Override
public void run() {
// TODO Auto-generated method stub
commonService.UseVipCode();
}
}
2.业务处理层service
@Service("commonService")
public class CommonService {
/**
* 使用VIPCode码
*/
public synchronized void UseVipCode(){
Map<String,Object> map = new HashMap<String, Object>();
//查询VIPCode信息
//根据VIPCode信息计算每一项的到期日期并且开通权限
Integer indexNum = CommonController.queue.poll();
String vipCode = ((HttpServletRequest) CommonController.requestQueue.get(indexNum)).getParameter("vipCode");
//调用使用VIPCode的接口
System.out.println("【正在处理请求:"+indexNum+"】VIPCode:"+vipCode);
try {
Thread.currentThread().sleep(0);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
map.put("requestNum", indexNum);
map.put("code", "0000");
map.put("message", "处理成功!");
CommonController.resultQueue.put(indexNum, map);
}
}
执行结果:
code0开始启动!
log4j:WARN No appenders could be found for logger (org.apache.commons.httpclient.HttpClient).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
{"requestNum":1,"code":"0000","message":"处理成功!"}
code1开始启动!
{"requestNum":2,"code":"0000","message":"处理成功!"}
code2开始启动!
{"requestNum":3,"code":"0000","message":"处理成功!"}
code3开始启动!
{"requestNum":4,"code":"0000","message":"处理成功!"}
code4开始启动!
{"requestNum":5,"code":"0000","message":"处理成功!"}