
商家的域名在A站使用,有买家保存了结算页地址。后来商家将域名绑定到B站使用。买家再次访问保存的地址时,弹出无法找到该笔订单的商品信息。原因是订单是在A站产生的,商品也是属于A站的。当域名绑到B站后,再次访问域名,就会解析到B站,而B站是没有订单上的商品ID的,因此无法找到该笔订单的商品信息。
造成上面的原因是因为结算页的临时订单信息表是没有存储站点主键ID,导致加载结算页仍会继续去寻找订单上的商品。
给结算页的临时订单信息表添加一个字段
shop_id,存储站点主键ID。根据结算页对应的token去寻找结算页的临时订单信息表,找到shop_id。判断该shop_id是否与当前域名指向的shop_id是否一致。不一致则直接抛异常说明“不存在此订单”。
由于是新增字段,插入数据时需要补上该字段。而历史数据需要做数据补偿。
临时订单的数据如下:

截图中已存在shop_id,暂且不用管他,因为这个已经是笔者实现了的解决方案,已完成数据补偿。
目标是要找到每个临时订单对应的站点主键ID
shop_id。正式订单的数据包含token,但是临时订单不一定就被买家付款从而成为正式订单。因此不能通过token找对应的shop_id。param中含有productId(存储spui主键ID),而productId必定是会对应一个shop_id,因此我们可以考虑通过productId找shop_id。但笔者在实现的过程中发现有些临时订单根本没有productId,却有variantId(存储sku主键ID)。因此,分析下来,通过productId或者variantId寻找shop_id。
上一小节分析得出,通过
productId或者variantId寻找shop_id。
引入缓存。待数据补偿的数据有500w行左右,如果每次都用product_id或者variant_id去查数据库,会给数据库增加很大压力,同时生产环境上也有流量打到数据库。因此引入缓存,把product_id与shop_id的关系缓存起来。每次先查缓存,差不多再统一批量查询。
引入LRU缓存。生产环境上同样也会有流量打到Redis,不考虑使用Redis。考虑使用JVM缓存`,比如LRU缓存,更加轻量,也不会影响生产环境的主业务。
MQ:
MQ有重试机制,并且消费者也是可以有多个线程去消费,并且MQ内部会维护一个线程池,当消费者线程满了后,MQ则不会继续发送消息给消费者消费。总之就是开发者不需要考虑线程池的使用,MQ会维护好。但是使用MQ创建Topic、消费者Group,还需要写消费者的代码并发版上线。而且也会提高MQ的负载导致影响正常业务使用MQ。
多线程:
使用多线程会更加轻量,并且不会影响生产环境MQ的负载,也不需要创建Topic、消费者Group(需要写代码并发版,上线。而多线程则只需用xxl-job的一个glue模式即可,热插拔,粘贴代码到xxl-job上面即可。)但是多线程需要小心使用线程池的几个核心参数,如核心线程数、最大线程数、阻塞队列等。
决定采用:
引入多线程。总共需要更新500w行。每个线程负责各自范围的数据(用select * from t_checkout_order where id >= a and id <= b,目的是用上where走主键索引)。比如线程A处理0-1000行,线程B处理1001-2000行。每个线程里面组装好待更新的数据,假如1000条数据里面,有3种shop_id,则按照shop_id分类更新(即update t_checkout_order set shop_id = #{shopId} where id in ();,目的是为了减少提交sql的次数,提高MySQL的执行效率)。
引入ThreadLocal。多个线程操作同一个LRU缓存,会出现线程安全问题。引入ThreadLocal,每个线程都有属于自己的一份LRU缓存数据。线程用完ThreadLocal后,需要手动执行remove()清理ThreadLocal的值避免内存泄漏。
LinkedHashMap实现。ThreadLocal存储LRU缓存,这样每个线程拥有各自的LRU缓存。根据product_id和variant_id从LRU缓存查shop_id,将所有查不到shop_id的product_id或者variant_id收集起来,批量一起查询数据库。查完再写入LRU缓存。shop_id分类,一整批提交给MySQL更新。ArrayBlockingQueue 的数组实现有界队列,限制队列大小为100。提交任务前,判断队列是否已经满了,如果满了,则主线程休眠5s 。一直循环判断,直到队列有空间存储新提交的任务。这样做的原因是防止队列满了并且已达到最大线程数导致触发拒绝策略,因为选用默认的拒绝策略会抛出异常。虽然抛出异常不会影响其他线程的执行,但是最好让线程池掌握在自己控制中,阻塞队列满了后适当阻塞会比较好。同时我的sql支持增量处理,已处理的数据不会重复处理。(3)使用默认的线程池拒绝策略ThreadPoolExecutor.AbortPolicy,线程数达到最大线程数并且阻塞队列已经满了,直接抛异常,快速失败减少无效的资源占用。针对未补偿的数据再执行多一次任务调度即可(因为sql脚本只会查询并处理未补偿的数据)。