文档参考:书名:《从程序员到架构师:大数据量、缓存、高并发、微服务、多团队协同等核心场景实战》-王伟杰
前文如下:
2.实现思路
看起来很简单,但该方案在具体实施过程中要考虑6个问题。
1)写请求与批量落库这两个操作同步还是异步?
2)如何触发批量落库?
3)缓冲数据存储在哪里?
4)缓存层并发操作需要注意什么?
5)批量落库失败了怎么办?
6)Redis的高可用配置。
2.1 写请求与批量落库这两个操作同步还是异步在回答这个问题前,先来对比一下同步与异步。
在回答这个问题前,先来对比一下同步与异步。
对于同步,写请求提交数据后,当前写操作的线程会等到批量落库完成后才开始启动。这种设计的优点是用户预约成功后,可在“我的预约”页面立即看到预约数据;缺点是用户提交预约后,还需要等待一段时间才能返回结果,且这个时间不定,有可能需要等待一个完整的时间窗。
对于异步,写请求提交数据后,会直接提示用户提交成功。这种设计的优点是用户能快速知道提交结果;缺点是用户提交完成后,如果查看“我的预约”页面,可能会出现没有数据的情况。
那到底应该使用哪种设计模式呢?下面再介绍下这两种设计模式的复杂度。
同步的实现原理是写请求提交数据时,写请求的线程被堵塞或者等待,待批量落库完成后再发送信号给写请求的线程, 这个线程获得落库完成的信号后,返回预约成功提示给用户。
不过,这个过程会引出一系列的问题,比如:
1)用户到底需要等待多久? 用户不可能无限期等待下去,此时还需要设置一个时间窗,比如每隔100毫秒批量落库一次。
2)如果批量落库超时了怎么办? 写请求不可能无限期等待,此时就需要给写请求线程的堵塞设置一个超时时间。
3)如果批量落库失败了怎么办? 是否需要重试?多久重试一次?
4)如果写请求一直堵塞,直到重试成功再返回吗?那需要重试几次? 这些逻辑其实与Spring Cloud组件、Hystrix请求合并功能(Hystrix 2018年已经停止更新)等类似。
如果使用异步的话,上面的第2)点、第4)点基本不用考虑,从复杂度的角度来看,异步比同步简单很多,因此项目直接选用异步的方式,预约数据保存到缓存层即可返回结果。
关于异步的用户体验设计,共有两种设计方案可供业务方选择。
1)在“我的预约”页面给用户一个提示:您的预约订单可能会有一定延迟。
2)用户预约成功后,直接进入预约完成详情页,此页面会定时发送请求去查询后台批量落库的状态,如果落库成功,则弹出成功提示,并跳转至下一个页面。其实,第一种方案在实际应用中也经常遇到,不过项目中主要还是使用第二种方案。因为在第二种方案中,大部分情况下用户是感受不到延迟的,用户体验比较好,而如果选择第一种方案,用户还要去思考:这个延迟是什么意思?是不是失败了?这无形中就影响了用户体验。
2.2 如何触发批量落库
关于批量落库触发逻辑,目前共分为两种。
1)写请求满足特定次数后就落库一次,比如10个请求落库一次。按照次数批量落库的优点是访问数据库的次数变为1/N,从数据库压力上来说会小很多。不过它也存在不足:如果访问数据库的次数未凑齐N次,用户的预约就一直无法落库。
2)每隔一个时间窗口落库一次,比如每隔一秒落库一次。按照时间窗口落库的优点是能保证用户等待的时间不会太久,其缺点是如果某个瞬间流量太大,在那个时间窗口落库的数据就会很多,多到在一次数据库访问中无法完成所有数据的插入操作(比如一秒内堆积了5000条数据),它们只能通过分批次来实现插入,这不就变回第一种逻辑了吗?
那到底哪种触发方式好呢?当时项目采用的方案是同时使用这两种方式。
具体实现逻辑如下。
1)每收集一次写请求,就插入预约数据到缓存中,再判断缓存中预约的总数是否达到一定数量,达到后直接触发批量落库。
2)开一个定时器,每隔一秒触发一次批量落库。
2.3 缓存数据存储在哪里
缓存数据不仅可以存放在本地内存中,也可以存放在分布式缓存中(比如Redis),其中最简单的方式是存放在本地内存中。
但是,Hystrix的请求合并也是存放在本地内存中,为什么不直接使用Hystrix?这是因为写缓存与Hystrix的请求合并有些不一样,请求合并更多考虑的是读请求的情况,不用担心数据丢失,而写请求需要考虑容灾问题:如果服务器宕机,内存数据就会丢失,用户的预约数据也就没有了。其实也可以考虑使用MQ来当缓存层,MQ的一个主要用途就是削峰,很适合这种场景。
不过这个项目选择了Redis,接下来需要考虑批量落库的设计了。批量落库主要是把Redis中的预约数据迁移到数据库中。而当新的数据一直增加时,批量落库可能会出现多个线程同时处理的情况,此时就要考虑并发性了。
2.4 批量落库失败了怎么办
在考虑落库失败这个问题之前,先来看看批量落库的实现逻辑。
1)当前线程从缓存中获取所有数据。因为每10条执行一次落库操作,不需要担心缓存中的数据量过多,所以也不用考虑将获得的数据分批操作了。
2)当前线程批量保存数据到数据库。
3)当前线程从缓存中删除对应数据(注意:不能直接清空缓存的数据,因为新的预约数据可能插入到缓存中了)。
以上各个步骤失败时的应对措施见表5-1。