《大量并发的应急处理方案与实践》提供的方法,只能做应急时的一种参考,不推荐在设计之初时使用,设计之初我们应该采用更合理的架构,以避免文中所提及的问题发生,关于架构请参考我的另一篇文章《开餐馆与做软件——如何提高大型网站性能》。
资源可以分成两种,一种为禁止并发访问的资源,另一种为允许并发访问的资源。禁止并发访问的资源如高速公路收费站,每一个收费口一次只处理一个通过请求,当有多个车辆请求通过时,解决的办法有两种,一种是增设收费口,另一种就是让车辆排队等候,即不马上处理请求,让请求等待空闲资源,我把这种解决办法称作后向异步处理,即将需要处理的请求,延后一段时间处理,这种方法适用于用户不需要看到即使效果的情况。关于“后向异步处理” 的例子请参考《大量并发的应急处理方案与实践1——异步处理》。允许并发访问的资源如商场,任何顾客都可以随时购买需要的商品,早先的大部分商场商品前都有一个柜台,顾客通过售货员购买需要的商品,当顾客增多后,售货员就会应接不暇,让顾客排队是一个办法,但是没有哪个商场会强制顾客排队购买商品,这种“后向异步处理”不适合解决这样的问题,于是一种更好的解决方案产生了——“超市”,取出部分商品直接摆放在售货台上,让顾客自己去取,然后只需要安排一个服务人员随时观察,发现商品不全再到库房中取来相应商品进行补充。
即将如下流程:
顾客 -> 售货员1 -> 商品1 -> 付费
顾客 -> 售货员2 -> 商品2 -> 付费
顾客 -> 售货员3 -> 商品3 -> 付费
改造为
库房 -> 服务员 —> 售货台(缓存)(前向异步处理)
顾客 -> 商品(1、2、3)-> 排队付费(后向异步处理)
这是一个典型例子,顾客对于付费的请求不需要即时响应,我们可以采用后向异步处理的方法。但是对于查看商品的请求越早满足越好,因此我们采用另一种前向异步处理的办法,将商品(数据)提前放到售货台(缓存)里,这样当顾客(请求)需要的时候可以直接取走。下面我们来看一个你可能会遇到的例子:
有一天你的老板突然接到电话,客户抱怨说当登录人数增加的时候,页面打开的非常慢,这时老板找到你,你竭尽全力向老板解释,老板用无奈的眼神望着你,对你说:"用户很急,全靠你了。"
你垂头丧气的走回到座位上,开始一点点排查原因,这时你发现原来这些页面在打开时,伴随大量的数据库操作,
这就是页面打开速度慢的原因。遇到这样的问题,比较简单快速的改造方法就是使用缓存。
即是将如下结构,
用户-> 页面与逻辑 -> 数据库
改造为:
数据库 -> 监控程序 -> 缓存 (前向异步处理)
用户-> 页面与逻辑 -> 缓存
举例来说你发现需要加速的页面 test.jsp, 其通过TestDB.java 类获取数据。
public class TestDB{
...
// 读取数据库 1
String sql1 = "..."; //查询语句
List tmplist1 = queryBySQL(sql1); //queryBySQL 方法,执行sql 语句,执行结果放到一个List 中返回
...
// 读取数据库 2
String sql2 = "..."; //查询语句
List tmplist2 = queryBySQL(sql2); //queryBySQL 方法,执行sql 语句,执行结果放到一个List 中返回
...
// 读取数据库 3
String sql3 = "..."; //查询语句
List tmplist3 = queryBySQL(sql3); //queryBySQL 方法,执行sql 语句,执行结果放到一个List 中返回
...
}
首先,我们创建一个与页面相对应的缓存模块(例如Buffer)。为需要缓存数据的页面创建一个与之对应的类TestBuffer.java 用来存放数据。必须保证TestBuffer.java 中的数据与数据库中相应数据的一一对应关系,因此我们将TestBuffer.java 设计为一个单例。
#Buffer
-- TestBuffer.java
public class TestBuffer{
private volatile static TestBuffer singleton=null;
private TestBuffer(){}
public static TestBuffer getInstance()
{
if(singleton==null){
synchronized(TestBuffer.class)
{
singleton=new TestBuffer();
}
}
return singleton;
}
}
我们分析需要加速页面的数据库操作模块,找出所有数据库操作点。例如我们分析TestDB.java ,发现如下数据库
操作点:读取数据库 1、读取数据库 2、读取数据库 3。这三个操作点分别返回三个List 对象,这三个List 对象就是我们要缓存的数据。我们将这三个对象加入到testBuffer.java 中。
public class TestBuffer{
private volatile static TestBuffer singleton=null;
private TestBuffer(){}
private static List tmplist1 = null
private static List tmplist2 = null
private static List tmplist3 = null
public static TestBuffer getInstance()
{
if(singleton==null){
synchronized(TestBuffer.class)
{
singleton=new TestBuffer();
}
}
return singleton;
}
public List getTmplist1(){
return tmplist1;
}
public List setTmplist1(List tmplist1){
this.tmplist1 = tmplist1;
}
public List getTmplist2(){
return tmplist2;
}
public List setTmplist2(List tmplist2){
this.tmplist2 = tmplist2;
}
public List getTmplist3(){
return tmplist3;
}
public List setTmplist3(List tmplist3){
this.tmplist3 = tmplist3;
}
}
重新改造原先的数据库访问模块TestDB.java ,更改如下:
public class TestDB{
...
//String sql1 = "..."; //查询语句
//String sql2 = "..."; //查询语句
//String sql3 = "..."; //查询语句
//List tmplist1 = queryBySQL(sql1); //queryBySQL 方法,执行sql 语句,执行结果放到一个List 中返回
//List tmplist2 = queryBySQL(sql2); //queryBySQL 方法,执行sql 语句,执行结果放到一个List 中返回
//List tmplist3 = queryBySQL(sql3); //queryBySQL 方法,执行sql 语句,执行结果放到一个List 中返回
List tmplist1 = TestBuffer.getTmplist1();
List tmplist2 = TestBuffer.getTmplist2();
List tmplist3 = TestBuffer.getTmplist3();
...
}
然后我们添加从数据库到缓存的前向异步处理模块,即我们实现一个提前运行的监控程序,将数据库中的数据放到缓存中,并保持缓存与数据库中的数据同步,我们可以使用线程实现。
public class testThread implements Runnable{
private static long interval = 3000; //循环间隔
@Override
public void run(){
while(true){
...
String sql1 = "..."; //查询语句
String sql2 = "..."; //查询语句
String sql3 = "..."; //查询语句
List tmplist1 = queryBySQL(sql1);
TestBuffer.setTmplist1(tmplist1);
List tmplist2 = queryBySQL(sql2);
TestBuffer.setTmplist2(tmplist2);
List tmplist3 = queryBySQL(sql3);
TestBuffer.setTmplist3(tmplist3);
...
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
以上提供的方法只做您遇到类似问题时的一种参考,是术,方法千变万化而道理不离其宗,合理使用缓存和异步处理是解决这一类问题的核心。