(一)背景介绍
我们在购物网站进行下订单时,理论上我们在下完订单之后,该订单所对应的商品“已售数量”应该实时更新。但若实时更新,带来的一个问题就是需要和数据库进行实时交互,增大了数据库的IO压力。但是,实际上对于商品“已售数量”并不需要实时显示出来,只需要每隔一段时间统计更新即可,并不影响用户购物体验。
对于像“商品已售数量”这类的“伪实时数据”,我们一般是采用一个定时任务,按照一定的周期来对这些“伪实时数据”进行统计,从而减少数据库的IO压力,提升了服务器的反应时间,却没有降低用户体验!
(二)难点介绍
对于这类问题解决的基本思路是:
遍历订单,将订单的数量取出来,并找到对应的商户,将商户的已售数量+订单中的数量=商户的最新已售数量。这里用sql语句即可完成:
<update id="synchronizedNumber" parameterType="Date">
<if test="lastUpdateTime == null">
UPDATE business b,(SELECT business_id,num FROM orders)o
SET b.number=b.number + o.num WHERE b.id=o.business_id
</if>
</update>
问题出现了:我们每次在启动定时任务时,都需要将所有的订单扫描一遍,重复统计了数据,这无形中又增加了数据库的IO压力,同时也增加了执行时间。怎么样进行优化?
解决办法:核心思想就是空间换时间。我们可以新建一个同步表(syntime),表里有三个字段(id,type(同步数据类型),lastUpdateTime(最后一次同步时间))。每次在同步之前,先读取lastUpdateTime,然后将当前系统时间更新到“lastUpdateTime”,最后开始同步。同步的sql语句:
<update id="synchronizedNumber" parameterType="Date">
<if test="lastUpdateTime == null">
UPDATE business b,(SELECT o.business_id, SUM(o.num) total
FROM (SELECT business_id,num FROM orders WHERE createDate<=#{currentTime})o GROUP BY o.business_id)temp
SET b.number=b.number + temp.total WHERE b.id=temp.business_id;
</if>
<if test="lastUpdateTime != null">
UPDATE business b,
(SELECT o.business_id, SUM(o.num) total
FROM (SELECT business_id,num FROM orders WHERE createDate>#{lastUpdateTime} AND createDate<=#{currentTime})o GROUP BY o.business_id)temp
SET b.number=b.number + temp.total WHERE b.id=temp.business_id;
</if>
</update>
这里又涉及到另外一个问题:“为什么是先更新lastUpdateTime,再同步,而不是先同步再更新lastUpdateTime?”
关于这个问题,我是这么想的:假设我们先同步,那么我们同步是需要一定时间的,有可能我们刚同步完数据库中的订单,新的订单又接连不断的进来,那么我们只能继续同步,白白延长了我们的同步时间。所以我们需要对同步时间进行规定,以服务器当前时间节点为基准,当前时间以前,lastUpdateTime时间以后的订单需要同步,之后产生的所有订单等待下次同步。所以我们需要先更新lastUpdateTime为当前服务器时间,再进行同步操作。
(三)应用案例
1.建同步表即对应的java类
CREATE TABLE `syntime` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`type` varchar(25) DEFAULT NULL COMMENT '同步的数据类型,用英文单词表示',
`updateTime` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/**
* @author **
* @date 2018/6/10 20:44
*/
public class SynchronizedTime extends BaseBean {
/**
* 同步的数据类型,我这里写成常量
*/
private String type;
/**
* 最后一次的同步时间
*/
private Date updateTime;
}
2.新建同步类和同步方法
/**
* @author **
* @date 2018/6/10 19:42
* “@Component("businessTask")”注解:这里面的value("businessTask"),我们在配置文件中需要使用该值,用以
* 指向任务调度类(BusinessTask)
*/
@Component("businessTask")
public class BusinessTask {
@Autowired
private BusinessDao businessDao;
@Autowired
private SynchronizedTimeDao synchronizedTimeDao;
/**
* 同步商品已售数量
* 核心思想:
* 1、建一张同步表,表中记录各种类型数据最后一次同步时间
* 2、根据数据类型从同步表中取出最后一次同步时间
* (1)若为NULL,则说明该类型数据还未同步过,所以需要查询所有订单,联合商户表,更新商户已售数量
* (2)不为NULL,同步订单,条件:最后一次同步时间 < 订单创建时间 <= 当前时间
*/
public void synchronizedNumber(){
//取出“商户已售数量”最后一次同步时间
Date lastUpdateTime = getLastSynchronizedTime(SynchronizedTimeConstant.BUSINESS_SOLD_NUMBER);
//以当前时间为节点,当前时间及其以前的为待同步对象
//
/*
* 首先更新同步时间
* 问题:为什么不是先同步,再更新同步时间?
* 原因:因为同步是需要一定时间的,而在我们同步的时候,还会有新的数据产生。若我们以同步结束时间点为界,那么在
* 开始同步到同步结束这段时间产生的数据在下一次同步操作中,也不会被同步。因为我们下次同步是以上次同步
* 结束时间点为开始查询条件的。
* 所以,这里统一以服务器当前时间为准(执行到sql语句也需要一定时间,所以若我们使用sql语句中的当前时间,
* 还是有时间差)
*/
Date currentTime = new Date(System.currentTimeMillis());
if (lastUpdateTime == null){
//若为null,说明暂未该同步记录,新增同步记录
SynchronizedTime synchronizedTime = new SynchronizedTime();
synchronizedTime.setType(SynchronizedTimeConstant.BUSINESS_SOLD_NUMBER);
//最后一次同步时间就是执行完本次同步的当前时间
synchronizedTime.setUpdateTime(currentTime);
synchronizedTimeDao.insertSynchronizedTime(synchronizedTime);
} else {
//若不为null,说明本来又该类型数据同步记录,只要更新最后一次同步时间为当前时间即可
synchronizedTimeDao.updateSynchronizedTime(SynchronizedTimeConstant.BUSINESS_SOLD_NUMBER, currentTime);
}
//开始同步
businessDao.synchronizedNumber(currentTime, lastUpdateTime);
}
private Date getLastSynchronizedTime(String type) {
Date lastUpdateTime;
//根据同步数据类型,取出该类型最后一次同步时间
SynchronizedTime synchronizedTime = synchronizedTimeDao.selectSynchronizedTimeByType(type);
if (synchronizedTime == null){
lastUpdateTime = null;
} else {
lastUpdateTime = synchronizedTime.getUpdateTime();
}
return lastUpdateTime;
}
}
3.同步“businessDao.synchronizedNumber(currentTime, lastUpdateTime);”对应的sql语句:
<update id="synchronizedNumber" parameterType="Date">
<if test="lastUpdateTime == null">
UPDATE business b,(SELECT o.business_id, SUM(o.num) total
FROM (SELECT business_id,num FROM orders WHERE createDate<=#{currentTime})o GROUP BY o.business_id)temp
SET b.number=b.number + temp.total WHERE b.id=temp.business_id;
</if>
<if test="lastUpdateTime != null">
UPDATE business b,
(SELECT o.business_id, SUM(o.num) total
FROM (SELECT business_id,num FROM orders WHERE createDate>#{lastUpdateTime} AND createDate<=#{currentTime})o GROUP BY o.business_id)temp
SET b.number=b.number + temp.total WHERE b.id=temp.business_id;
</if>
</update>
4.启动任务调度
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">
<!-- Spring3.0以后自带的任务调度 配置文件 -->
<!-- 扫描task包下的任务调度类,实现向Spring容器注册 -->
<context:component-scan base-package="com.imooc.task"/>
<!-- 配置任务调度 -->
<task:scheduled-tasks>
<!--ref参数指定的即任务类,method指定的即需要运行的方法,cron及cronExpression表达式-->
<!--每小时同步一次商品已售数量-->
<task:scheduled ref="businessTask" method="synchronizedNumber" cron="0 0 0/1 * * ?"/>
</task:scheduled-tasks>
</beans>