在一些需要实时消息功能的网站应用中,除了客户端轮询请求服务器获取消息外,还有一种方案就是comet长连接推送消息。显然后者更具有优势,实时性高,客户端、服务端压力都比较小。对于长连接方案,我们需要考虑对长连接进行管理,以便有消息时可以推送到客户端。可是对于大量长连接的建立服务器是有可能会被压垮的,所以不是一味的接受连接和hold住连接就可以的,需要对连接进行超时管理,如果超时需要推送重连消息或是关闭连接。我们可以初略分析下具体的管理逻辑。
先简单分析下具体的应用场景:用户登录一个电商网站(苏宁,国美),恰好在大量客户登录之后,商家(海尔、格力等)推出了一系列优惠活动(针对注册用户),需要及时推送到浏览器让用户了解,不需要用户刷新页面。
就这么一个常见的功能,我们需要怎么做呢?首先,用户打开页面,客户端js就需要请求长连接,并携带用户信息(id)过来,服务器使用list、queue之类的存储起来,记录当时的请求时间;另起一个守护线程,扫描是否有超时或需要断开的连接,超时的需要发重连消息,断开的发断开消息;如果有消息需要推送从list、queue中获取连接推送即可。好像没什么问题,对于超时处理,遍历大量元素耗时太长是接受不了的,那么排除list存储,queue具有FIFO的特性,每次检查超时,只检查表头元素即可,这样是很快。但是忽略了重连这个问题,间隔时间内重连,不能重新new一个连接对象(大量新对象耗内存伤不起),只能复用之前的对象,之前对象的time就会发生变化,queue就会变成不按时间排序的有序队列了,如果按之前的处理方式就会阻塞在表头,无法处理了,并且queue的重连遍历也是个问题,队列本身遍历比较麻烦。从现在来看单一的数据结构很难处理这样的问题,需要多个数据结构结合使用。我们知道在linux内核中定时器的实现采用wheel。我们可以借鉴之,对于超时的处理我们使用hashedwheel,hashed+wheel,hash主要用于对时间进行处理,决定放在wheel的那个区间段中;wheel是一个数组,减小了遍历的数目。
关于HashedWheelTimer实现具体见代码。netty中有类似的实现,这里进行了简化。主要有:1. 守护线程由外部提供,该线程来调用hashedwheel;2. 假定我们的超时时间不会大于wheel的圈时长(假设wheel 1s转动一次,一共512个区间,圈时长就为512s,我们的超时时间不会这么久),就不会有第几圈计算的问题;
package com.yq.algorithmic;
/**
*
*
* 描述:连接管理
* 创建时间:2011-9-16下午08:42:34
* @author yq76034150
*
*/
public class TimeOut {
final long time;
final long deadline;
private int rounds;
//连接的用户id
//连接的channel
public TimeOut(long time, long deadline) {
super();
this.time = time;
this.deadline = deadline;
}
public int getRounds() {
return rounds;
}
public void setRounds(int rounds) {
this.rounds = rounds;
}
public long getTime() {
return time;
}
public long getDeadline() {
return deadline;
}
public String toString(){
return "survival: " + (System.currentTimeMillis() - time);
}
}
package com.yq.algorithmic;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
*
* 描述:一个基于时间hash的wheel超时器
* 创建时间:2011-9-16上午11:44:02
* @author yq76034150
*
*/
public class HashedWheelTimer {
volatile int tick = 1;
volatile int currentWheel;
private Queue[] wheel;
final int mask;
private long delay;
//private int ticksPerWheel;
private long tickDuration;
//private long roundDuration;
/**
* 描述:
* @param ticksPerWheel 一圈多少tick
*/
public HashedWheelTimer(int ticksPerWheel, long tickDuration, long delay){
mask = ticksPerWheel - 1;
this.delay = delay;
//this.ticksPerWheel = ticksPerWheel;
this.tickDuration = tickDuration;
//this.roundDuration = tickDuration * ticksPerWheel;
createWheel(ticksPerWheel);
}
/**
*
* 描述:wheel中填充数据
* @param timeout
*/
public void newTimeOut(TimeOut timeout){
long shift = delay / tickDuration;
//long remainRounds = delay / roundDuration;
int stopIndex = currentWheel + (int)shift & mask;
wheel[stopIndex].offer(timeout);
}
/**
*
* 描述:外部线程调用,外部线程调用间隔必须和tickDuration一致
*/
public void run(long start){
//模拟外部执行线程执行间隔数
try {
Thread.sleep(tickDuration);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
tick++;
currentWheel = currentWheel + 1 & mask;
long deadline = start + tick * tickDuration;
Queue queue = wheel[currentWheel];
TimeOut timeOut = queue.peek();
if(timeOut != null){
long firstDeadline = timeOut.getDeadline();
boolean isDead = firstDeadline <= deadline;
if(firstDeadline <= deadline){
while(isDead){ //对一个tick中的队列元素递归检查超时
queue.remove();
System.out.println("delete: " + currentWheel + timeOut);
timeOut = queue.peek();
if(timeOut != null){
firstDeadline = timeOut.getDeadline();
isDead = firstDeadline <= deadline;
} else {
break;
}
}
}
}
}
/**
*
* 描述:初始化wheel
* @param ticksPerWheel
*/
private void createWheel(int ticksPerWheel) {
wheel = new Queue[ticksPerWheel];
for(int i = 0; i < ticksPerWheel; i++){
wheel[i] = new ConcurrentLinkedQueue();
}
}
/**
* 描述:
* @param args
*/
public static void main(String[] args) {
long tickDuration = 1000;
long delay = 9000;
HashedWheelTimer timer = new HashedWheelTimer(4, tickDuration, delay);
long start = System.currentTimeMillis();
TimeOut timeout = new TimeOut(start, start + delay);
timer.newTimeOut(timeout);
timer.run(start);
start = System.currentTimeMillis();
timeout = new TimeOut(start, start + delay);
timer.newTimeOut(timeout);
timer.run(start);
for(int i = 0; i < 10; i++){
timer.run(start);
}
}
}
上面的例子使用queue,会存在阻塞问题。测试代码如下:
package com.yq.hash.timer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
*
* 描述:一个基于时间hash的wheel超时器
* 创建时间:2011-9-16上午11:44:02
* @author yq76034150
*
*/
public class HashedWheelTimer {
volatile int tick = 1;
volatile int currentWheel;
private Queue[] wheel;
final int mask;
private long delay;
//private int ticksPerWheel;
private long tickDuration;
//private long roundDuration;
/**
* 描述:
* @param ticksPerWheel 一圈多少tick
*/
public HashedWheelTimer(int ticksPerWheel, long tickDuration, long delay){
mask = ticksPerWheel - 1;
this.delay = delay;
//this.ticksPerWheel = ticksPerWheel;
this.tickDuration = tickDuration;
//this.roundDuration = tickDuration * ticksPerWheel;
createWheel(ticksPerWheel);
}
/**
*
* 描述:wheel中填充数据
* @param timeout
*/
public void newTimeOut(TimeOut timeout){
long shift = delay / tickDuration;
//long remainRounds = delay / roundDuration;
int stopIndex = currentWheel + (int)shift & mask;
wheel[stopIndex].offer(timeout);
}
/**
*
* 描述:外部线程调用,外部线程调用间隔必须和tickDuration一致
*/
public void run(long start){
//模拟外部执行线程执行间隔数
try {
Thread.sleep(tickDuration);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
tick++;
currentWheel = currentWheel + 1 & mask;
long deadline = start + tick * tickDuration;
Queue queue = wheel[currentWheel];
TimeOut timeOut = queue.peek();
if(timeOut != null){
long firstDeadline = timeOut.getDeadline();
boolean isDead = firstDeadline <= deadline;
//if(firstDeadline <= deadline){
while(isDead){ //对一个tick中的队列元素递归检查超时
queue.remove();
System.out.println("tick: " + tick + " delete: " + currentWheel + timeOut);
timeOut = queue.peek();
if(timeOut != null){
firstDeadline = timeOut.getDeadline();
isDead = firstDeadline <= deadline;
} else {
break;
}
}
//}
}
}
/**
*
* 描述:初始化wheel
* @param ticksPerWheel
*/
private void createWheel(int ticksPerWheel) {
wheel = new Queue[ticksPerWheel];
for(int i = 0; i < ticksPerWheel; i++){
wheel[i] = new ConcurrentLinkedQueue();
}
}
/**
* 描述:
* @param args
*/
public static void main(String[] args) {
long tickDuration = 1000;
long delay = 9000;
HashedWheelTimer timer = new HashedWheelTimer(4, tickDuration, delay);
long start = System.currentTimeMillis();
TimeOut timeout = new TimeOut(start, start + delay, 1);
timer.newTimeOut(timeout);
timer.run(start);
//4s后 保证和之前的落在一个区间
for(int i = 0; i < 3; i++){
timer.run(start);
}
TimeOut timeout2 = new TimeOut(System.currentTimeMillis(), System.currentTimeMillis() + delay, 2);
timer.newTimeOut(timeout2);
timer.run(start);
//更改表头的时间 queue结构的就会阻塞
timeout.setTime(start + 19000);
timeout.setDeadline(start + 19000 + delay);
timer.newTimeOut(timeout);
timer.run(start);
for(int i = 0; i < 30; i++){
timer.run(start);
}
}
}
tick: 30 delete: 1v:1316356889764 time:1316356879760 deadline: 1316356888760 survival: 10004
tick: 30 delete: 1v:1316356889765 time:1316356864763 deadline: 1316356873763 survival: 25002
tick: 31 delete: 2v:1316356890765 time:1316356879760 deadline: 1316356888760 survival: 11005
package com.yq.hash.timer;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
*
* 描述:一个基于时间hash的wheel超时器
* 创建时间:2011-9-16上午11:44:02
* @author yq76034150
*
*/
public class HashedWheelTimer {
volatile int tick = 1;
volatile int currentWheel;
private Map[] wheel;
final int mask;
private long delay;
//private int ticksPerWheel;
private long tickDuration;
//private long roundDuration;
/**
* 描述:
* @param ticksPerWheel 一圈多少tick
*/
public HashedWheelTimer(int ticksPerWheel, long tickDuration, long delay){
mask = ticksPerWheel - 1;
this.delay = delay;
//this.ticksPerWheel = ticksPerWheel;
this.tickDuration = tickDuration;
//this.roundDuration = tickDuration * ticksPerWheel;
createWheel(ticksPerWheel);
}
/**
*
* 描述:wheel中填充数据
* @param timeout
*/
public void newTimeOut(TimeOut timeout){
long shift = delay / tickDuration;
//long remainRounds = delay / roundDuration;
int stopIndex = currentWheel + (int)shift & mask;
wheel[stopIndex].put(timeout, true);
}
/**
*
* 描述:外部线程调用,外部线程调用间隔必须和tickDuration一致
*/
public void run(long start){
//模拟外部执行线程执行间隔数
try {
Thread.sleep(tickDuration);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
tick++;
currentWheel = currentWheel + 1 & mask;
long deadline = start + tick * tickDuration;
Map map = wheel[currentWheel];
TimeOut timeOut = null;
for(Entry entry : map.entrySet()){
timeOut = entry.getKey();
if(timeOut.getDeadline() <= deadline){
map.remove(timeOut);
System.out.println("tick: " + tick + " delete: " + currentWheel + timeOut);
}
}
}
/**
*
* 描述:初始化wheel
* @param ticksPerWheel
*/
private void createWheel(int ticksPerWheel) {
wheel = new Map[ticksPerWheel];
for(int i = 0; i < ticksPerWheel; i++){
wheel[i] = new ConcurrentHashMap();
}
}
/**
* 描述:
* @param args
*/
public static void main(String[] args) {
long tickDuration = 1000;
long delay = 9000;
HashedWheelTimer timer = new HashedWheelTimer(4, tickDuration, delay);
long start = System.currentTimeMillis();
TimeOut timeout = new TimeOut(start, start + delay, 1);
timer.newTimeOut(timeout);
timer.run(start);
//4s后 保证和之前的落在一个区间
for(int i = 0; i < 3; i++){
timer.run(start);
}
long start2 = System.currentTimeMillis();
TimeOut timeout2 = new TimeOut(start2, start2 + delay, 2);
timer.newTimeOut(timeout2);
timer.run(start);
//更改表头的时间 queue结构的就会阻塞
timeout.setTime(start + 30000);
timeout.setDeadline(start + 30000 + delay);
timer.newTimeOut(timeout);
timer.run(start);
for(int i = 0; i < 50; i++){
timer.run(start);
}
}
}