专栏名称: 亿级流量网站架构
开涛技术点滴
目录
相关文章推荐
程序员的那些事  ·  国企也中招!官网被挂上“码农的钱你也敢吞,* ... ·  昨天  
程序员小灰  ·  DeepSeek让我的朋友一夜暴富! ·  3 天前  
程序猿  ·  本地部署 DeepSeek ... ·  2 天前  
OSC开源社区  ·  Gitee邀您参与SBOM行业调研:共建可信 ... ·  5 天前  
51好读  ›  专栏  ›  亿级流量网站架构

超时与重试机制(2)—《亿级流量》

亿级流量网站架构  · 公众号  · 程序员  · 2017-07-02 10:08

正文

数据库客户端超时

在使用数据库客户端时,我们会使用数据库连接池,数据库连接池可以进行如下超时设置。

value="connectTimeout=2000; socketTimeout=2000 "/>


●     网络连接/读超时:使用connectionProperties配置Mysql超时时间,如果是Oracle则可以通过如下配置。

value="oracle.net.CONNECT_TIMEOUT=2000;oracle.jdbc.ReadTimeout=2000"/>


●     默认Statement超时时间,通过defaultQueryTimeout配置,单位是秒。

●     从连接池获取连接的等待时间,通过maxWaitMillis配置。

●     Statement超时,如果使用ibatis,则可以通过如下方式配置Statement超时。


因此我们只需要如下配置。

lazyLoadingEnabled="false"errorTracingEnabled="true" maxRequests="32"

defaultStatementTimeout="2"/>


defaultStatementTimeout单位是秒,根据业务配置。如果数据库连接池配置了,则此处可以不用配置。


如果想只设置某个Statement的超时时间,则可以考虑:


如上配置其实最终会调用Statement.setQueryTimeout方法设置Statement超时时间。


●    事务超时是总Statement超时设置,比如我们使用Spring管理事务的话,可以使用如下方式配置全局默认的事务级别的超时时间。


这里我们分析下为什么说事务超时是Statement超时的总和,此处我们分析spring的DataSourceTransactionManager,首先开启事务时会调用其doBegin方法。

//先获取@Transactional定义的timeout,如果没有,则使用defaultTimeout

int timeout =determineTimeout(definition);

if (timeout !=TransactionDefinition.TIMEOUT_DEFAULT) {

txObject.getConnectionHolder().setTimeoutInSeconds(timeout);

}


其中determineTimeout用来获取我们设置的事务超时时间,然后设置到ConnectionHolder对象上(其是ResourceHolder子类),接着看ResourceHolderSupport的setTimeoutInSeconds实现。

public voidsetTimeoutInSeconds(int seconds) {

setTimeoutInMillis(seconds* 1000);

}

public voidsetTimeoutInMillis(long millis) {

this.deadline = newDate(System.currentTimeMillis() + millis);

}


大家可以看到,此处会设置一个deadline时间,用来判断事务超时时间,那什么时候调用呢?首先检查该类中的代码,会发现。

public int getTimeToLiveInSeconds() {

double diff = ((double) getTimeToLiveInMillis()) /1000;

int secs = (int) Math.ceil(diff);

checkTransactionTimeout(secs <= 0);

return secs;

}

public long getTimeToLiveInMillis() throwsTransactionTimedOutException{

if (this.deadline == null) {

throw new IllegalStateException("No timeoutspecified for this resource holder");

}

long timeToLive = this.deadline.getTime() -System.currentTimeMillis();

checkTransactionTimeout(timeToLive <= 0);

return timeToLive;

}

private void checkTransactionTimeout(booleandeadlineReached) throws TransactionTimedOutException {

if (deadlineReached) {

setRollbackOnly();

throw newTransactionTimedOutException("Transaction timed out: deadline was " +this.deadline);

}

}


我们发现调用getTimeToLiveInSeconds和getTimeToLiveInMillis会检查是否超时,如果超时了,则标记事务需回滚,并抛出TransactionTimedOutException异常进行回滚。


DataSourceUtils.applyTransactionTimeout会调用DataSourceUtils. applyTimeout, DataSourceUtils.applyTimeout代码如下。

public static void applyTimeout(Statement stmt,DataSource dataSource, int timeout) throws SQLException {

ConnectionHolder holder =         (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);

if (holder != null && holder.hasTimeout()){

// 计算剩余的事务超时时间覆盖Statement超时

stmt.setQueryTimeout(holder.getTimeToLiveInSeconds());

} else if (timeout > 0) {

//如果没有配置事务超时,则使用Statement超时

stmt.setQueryTimeout(timeout);

}

}


在stmt.setQueryTimeout(holder.getTimeToLiveInSeconds())时会调用getTimeToLiveIn Seconds(),这会检查事务是否超时。在JdbcTemplate中,执行SQL之前,会调用其applyStatementSettings方法,其将调用DataSourceUtils.applyTimeout(stmt,getDataSource(), getQueryTimeout())设置超时时间。


此处有一个问题,如果设置了事务超时,Statement级别的就不起作用了,整体会使用事务超时覆盖Statement超时。

NoSQL客户端超时

对于MongoDB,我们使用的是spring-data-mongodb客户端,可以通过如下配置设置相关的超时时间。

connections-per-host="${mongo.connectionsPerHost}"

threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}"

max-wait-time="${mongo.maxWaitTime}"

connect-timeout="${mongo.connectTimeout}"

socket-timeout="${mongo.socketTimeout}"

socket-keep-alive="${mongo.socketKeepAlive}"

auto-connect-retry="${mongo.autoConnectRetry}" />

我们曾经就遇到过因为不设置mongodb客户端timeout而导致服务响应慢的情况。


对于Redis,我们使用的是Jedis客户端,可以通过如下配置分配等待获取连接池连接的超时时间和网络连接/读超时时间。

PoolJedisConnectionFactory connectionFactory = new PoolJedisConnectionFactory();

connectionFactory.setMaxWaitMillis(maxWaitMillis);

connectionFactory.setTimeout(timeoutInMillis);

Jedis在建立Socket时通过如下代码设置超时。

this.socket.connect(new InetSocketAddress(this.host, this.port),this. timeout);

this.socket.setSoTimeout(this.timeout);

可以在JVM启动时通过添加-Dsun.net.client.defaultConnectTimeout=60000-Dsun.net.client.defaultReadTimeout=60000来配置默认全局的Socket连接/读超时。即如Httpclient、JDBC等,如果没有配置socket超时,则默认会使用该超时。

业务超时

任务型:比如,订单超时未支付取消,超时活动自动关闭等,这属于任务型超时,可以通过Worker定期扫描数据库修改状态即可。还有如有时候需要调用的远程服务超时了(比如,用户注册成功后,需要给用户发放优惠券),可以考虑使用队列或者暂时记录到本地稍后重试。


服务调用型:比如,某个服务的全局超时时间为500ms,但我们有多处服务调用,每处的服务调用超时时间可能不一样,此时,可以简单地使用Future来解决问题,通过如Future.get(3000,TimeUnit.MILLISECONDS)来设置超时。

前端Ajax超时

我们使用jQuery来进行Ajax请求,可以在请求时带上timeout参数设置超时时间。

$.ajax({

url:"http://ins.jd.com:9090/test",

dataType:"jsonp",

jsonp:"test",

jsonpCallback:"test",

timeout:2000,

success:function(result,status,xhr) {

//success

},

error: function(result,status,xhr){

if(status== 'timeout') {

//timeout

}

}

});

当进行跨域JSONP请求时,使用jQuery 1.4.x版本时,IE9、Chrome 52、Firefox 49测试 JSONP时,请求在超时后不能被取消,即使客户端超时了,该脚本也将一直运行;使用jQuery1.5.2时超时是起作用了,但是,发出去的请求是没有取消的(请求还处于执行状态)。


如还有一种办法来进行超时重试,通过setTimeout进行超时重试,比如,京东首页的某个异步接口,其中一个域名(A机房)超时了,想超时后通过另一个域名(B机房)重新获取数据,代码如下所示。


var id = setTimeout(retryCallback, 5000);

$.ajax({

dataType: 'jsonp',

success:function() {

clearTimeout(id);

...

}

});

除了客户端设置超时外,服务端也一定要配置合理的超时时间。

总结

本文主要介绍了如何在Web应用访问的整个链路上进行超时时间设置。通过配置合理的超时时间,防止出现某服务的依赖服务超时时间太长而响应慢,以致自己响应慢甚至崩溃。


客户端和服务端都应该设置超时时间,而且客户端根据场景可以设置比服务端更长的超时时间。如果存在多级依赖关系,如A调用B,B调用C,则超时设置应该是A>B>C,否则可能会一直重试,引起DDoS攻击效果。不过最终如何选择还是要看场景,有时候客户端设置的就是要比服务端的超时时间短,通过在服务端实施限流/降级等手段防止DDoS攻击。







请到「今天看啥」查看全文