一、非实时数据的同步问题

(一)背景介绍
  我们在购物网站进行下订单时,理论上我们在下完订单之后,该订单所对应的商品“已售数量”应该实时更新。但若实时更新,带来的一个问题就是需要和数据库进行实时交互,增大了数据库的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">
        <!--lastUpdateTime == null 表示该数据类型暂未同步过-->
        <if test="lastUpdateTime == null">
            UPDATE business b,(SELECT business_id,num FROM orders WHERE #{currentTime}>=updateTime)o
            SET b.number=b.number + o.num WHERE b.id=o.business_id
        </if>
        UPDATE business b,(SELECT business_id,num FROM orders WHERE #{currentTime}>=updateTime AND updateTime>#{lastUpdateTime})o
        SET b.number=b.number + o.num WHERE b.id=o.business_id
    </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 business_id,num FROM orders WHERE #{currentTime}>=updateTime)o
            SET b.number=b.number + o.num WHERE b.id=o.business_id
        </if>
        UPDATE business b,(SELECT business_id,num FROM orders WHERE #{currentTime}>=updateTime AND updateTime>#{lastUpdateTime})o
        SET b.number=b.number + o.num WHERE b.id=o.business_id
    </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>

猜你喜欢

转载自blog.csdn.net/panchang199266/article/details/80646986