大家好,我是田螺。
相信很多后端开发的伙伴们,都做过刷数任务了吧。今天跟大家聊聊,做好一个刷数任务,需要具备哪些后端思维。
我们做刷数任务的时候,首先要考虑的是,这些被刷的数据是否还要还原的。或者刷出问题时,需要回滚的。如果是的话,我们就要做好备份。
如果你是把数据迁移到新的表,则有可能不需要备份,这个具体问题具体分析的哈。
通常,我们在一个事务内,先备份数据,再操作刷数逻辑。
图片
当然,备份数据的方式有多种方式,可以数据库备份,比如搞一个备份表。或者文件系统快照等,在需要的时候,就还原数据。
我们刷数的时候,先确认下具体的业务需求和数据模型。然后需要确定刷数的维度是什么。
图片
当然,还有其他维度,比如产品维度等等都可以,就看业务需求和你们的数据模型。
确认了刷数维度后,需要思考你的刷数是否支持灰度。比如确认客户维度刷数后,你实现的代码,是否支持灰度刷数,也就是说,是否支持先刷一部分客户,确认没问题后,再根据配置刷全量客户。
比如,你要给一个客户相关的业务表刷数,需要考虑并发场景,简单点说,就是你在刷数的过程中,客户是否可能在做交易请求,这时候,是否可能产生脏数据。
一般情况下,可以考虑给客户维度加分布式锁,比如加锁的key是customerId。但是我们加锁的时候,肯定是不希望影响客户太久的,因为加锁后,整个刷数的过程中,系统是处理不了这个用户的交易请求的,也就是说影响交易了。
图片
所以也一般刷数任务,我们会选择在夜深人静的时候执行,这时候用户发生交易的概率很低,影响相对较小。
我们在刷数的时候,有可能会刷失败。比如因为网络问题、或者目标表结构等等。失败后,我们有哪些措施保证呢:
图片
如果刷数失败的话,我们要确保数据的完整性和一致性,一般一个刷数维度,需要加事务,确保失败可以回滚,保证数据一致性。刷失败之后,首先确认分布式锁要被移除(如果有加锁的话),因为要确认即使失败后,交易也是能正常进行的。
还要考虑失败支持重试。可以自动重试或者手动重试,比如通过xxl-job定时任务捞取,继续重试。有时候,可以设置重试次数和重试间隔,确保任务在一段时间内尝试恢复。
我们第4小节提到,如果失败了,需要保证数据的完整性和一致性。其实,一般我们刷数,就是通过加事务处理去保证的。
事务需要加到恰到好处,比如我们不能所有的刷数业务都放到一个事务内。如果我们是按照客户维度来刷数的,我们就一个事务把一个客户所有的数据刷的逻辑放到一块,当然,有些查询是可以放到事务外处理的,一些更新或者修改、删除操作则放到事务内。
图片
如果你的数据是分库处理的,则有可能刷数的时候要考虑分布式事务了。
如果我们的刷数任务数据量很大,执行耗时比较久的话。就建议可以多线程并行执行。
比如你是分库分表的,是有30个表,你可以A线程执行1-10的表,B线程执行11-20的表,C线程执行21-30的表。
图片
我们执行一个刷数任务,一定要做好日志记录。
我记得我们技术领导说过一句话,很有道理:评价你的日志是否打印得是否够好。就是你根据控制台打印出的日志,能知道你的复杂业务执行到什么流程。如果异常中断,你能根据日志快速知道什么异常,哪个业务数据有问题,那就够了。
刷数日志打印,一般包括:
我们开发刷数逻辑的时候,如果某种返回不符合预期的时候,就需要告警上报监控(比如插入数据库返回跟预期插入条数不一致)。
又或者是你刷数失败,需要包这个异常日志打印出来,并且上报监控(比如普罗米修斯,和企业微信通知)。比如这块代码:
try{ flushService.flushDataCustomerLevel(customerNo);}catch(Exception e){ Logger.error("flush customer data fail: {}", customerNo, e); prometheusMonitor.report("刷数失败",customerNo); notify(); weChatWorkSend();}
如果你刷的数据量很大的时候,最好做压测。压测通常包括模拟多种负载情况,以确保系统在不同条件下都能正常运行。
做刷数任务压测,主要考虑这几方面:
刷数压测的好处:
图片
有些时候,我们如果担心刷数任务跑太久,可能会影响交易,这时候我们可以搞个配置变量,比如apollo配置变量,控制刷数多长时间后,可以停止。
我们做刷数任务的时候,经常是分页循环扫描某个客户/用户表,然后一批一批出来执行刷数逻辑。比如伪代码像这样:
long minId while(true){ List<CustomerDo> customerList = customer.pageQueryAscID(pageSize,minId); flushCustomerData(customerList); if(customerList.size()< pageSize){ break; } minId = customerList.get(customerList.size()-1).getId(); }
这块代码,其实没啥问题。有些时候,我们可能手抖写错了,可能导致死循环。
其实为了保护我们的系统,我们可以先确认下客户有多少,然后设置个循环次数,当超过最大循环次数之后,就告警排查确认。
long minId; Integer maxCycleNum =1000; Integer cycleNum = 0; while(true ){ List<CustomerDo> customerList = customer.pageQueryAscID(pageSize,minId); flushCustomerData(customerList); if(customerList.size()< pageSize){ break; } minId = customerList.get(customerList.size()-1).getId(); if(cycleNum>= maxCycleNum){ //告警 } cycleNum ++; }
当然这只是个一种后端思路哈。
大家如果使用过xxl-job作为定时任务,应该抖配置过它的路由规则吧。比如是分片的,还是第一个/最后一个等等。
如果是分片的,就是多个pod都可能执行到你的业务逻辑。这时候你要考虑并发执行,你的业务是否收影响。
我们做刷数任务的时候,很多时候,都要跟SQL打交道。
我们要确保查询、更新、或者删除的数据量大的表,都要有索引了。要确保没有慢SQL。
常规的我们可以用explain分析SQL,我们还可以通过压测分析出来。
如果你是按照客户维度刷数,加了客户维度的分布式锁,你要考虑锁时间是多久,锁时间是否可以配置(一般这种最好配置一下。)
如果你时间设置小,那这时候刷数还没完成,锁就超时释放了,那不就有问题啦。
如果你时间设置过长也不太好,当然,你在刷完数,finally执行释放锁也可以。
finally { redisService.deleteKey(customerNoKey); }
我们在刷数的时候,为了保证数据的完整性和一致性,一般要求加事务的。
但是,切忌事务不要太大,我们可以把一些查询放到事务外,把计算逻辑也放事务外,把数据库的更新、新增、删除操作放到事务内就好。
就是把大事务拆分为小事务。
一般来说,做刷数,尽量打印一下耗时,这样我们可以根据日志,观察是否有哪些问题需要及时处理的。
比如打印刷一个客户要多久。或者打印一批客户要多久,等等。
有些时候,我们需要配置一定的灰度规则来支持灰度刷数。如果刷数流程是先扫描所有客户,然后接着判断客户是否命中灰度。这样每次任务执行,都会扫描客户表。
我么可以考虑加个配置,传特定的客户号,根据客户号列表,去查客户列表,然后开始刷数逻辑,不用再全表扫描客户表了。
有些时候,我们没法确认我们刷的逻辑是否正确,这时候,可以考虑是否加校验逻辑。
你可以异步进行校验,也可以同步校验(当然,如果耗时不大的时候)
如果我们是按照一个客户维度去刷数的,你要确认A客户刷失败,是不是不能影响B客户。这时候建议try...catch 包住可能的异常,这样即有利于分析错误原因,又可以不不因为未知异常导致刷数中断。
本文链接://www.dmpip.com//www.dmpip.com/showinfo-26-76543-0.html实现一个刷数任务,需要思考哪些维度?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 用 Python 优雅地玩转 Elasticsearch:实用技巧与最佳实践
下一篇: 垃圾收集器的秘密:深入理解JVM性能调优