城市直播房产教育博客汽车
投稿投诉
汽车报价
买车新车
博客专栏
专题精品
教育留学
高考读书
房产家居
彩票视频
直播黑猫
投资微博
城市上海
政务旅游

MySQL分表篇该如何将月增上亿条数据的单表处理方案优雅落地

2月6日 失了心投稿
  分库分表实战内容基本上很少有人去分享,在网上能够搜出来的也大多属于一些方法论,但大部分技术开发真正缺少的恰恰是这些实操经验,所以后续的内容多以实践为主,携手诸位真正彻底悟透分库分表相关的技术。
  尤其是对于库内分表这块的分享,当你去搜索单表数据增长过快该如何处理时,一般都会推荐你做分表处理,但你几乎找不到较为全面的实操教学,网上讲述分表技术更多是停留在表面的理论概念层次做阐述,而本章中则会结合自身之前接触的一个项目业务,再对库内分表技术进行全面阐述
  PS:虽然当时负责的项目并未达到月增上亿条数据的规模,但处理这种单表数据过大的方案都是一致的,将本文看完最后,无论单月数据增长多少,几百万条、几千万条、甚至几亿条。。。。,相信诸位都能具备处理这类业务的能力!一、源自于软硬结合的特殊业务
  在讲本次主题之前,先来聊聊之前碰到的这个业务,这个业务比较特殊,相信很多小伙伴从未碰到过,这种业务本身用户量大,甚至可以说用户量非常非常少,因为业务的起源来自于一款硬件设备,但具体的设备类型由于某些缘故就不透露了,可以理解成是下面这个东东:
  虽然当时的硬件设备并不是这个,但也和它很类似,相信大家但凡在超市购过物都认识它,也就是超市收银台的收银机,当时我们是对外提供了一千台设备,这种设备通常一台只有一个用户,所以当时整个系统上线后所有的用户加起来,涵盖后台管理员、超级管理员账号在内,也不过1200个用户,这个用户规模相较于常见业务而言属实不多。
  而当时我们需要负责的就是:为这些设备开发一个操作系统,这里不是指Windows、Linux、Mac这类嵌入式的底层系统,而是给机器的操作员开发一个操作界面,就类似于诸位在超市购物时,超市收银员用手操作的那个界面。
  因为这些机器本身会安装一个带UI的系统,里面也支持安装一些软件,我们的软件会以GUI的形式嵌入这些设备,当时我要干的就是直接开发API接口,然后提供给GUI界面界面调用。本质上就属一个前后端分离的项目,只不过前端从原本的Web界面变成了GUI界面。
  大家听起来这个项目是不是特别容易完成,用户量又少代表不需要考虑并发,也不会存在太大的流量冲击,性能要求也不会太高,似乎就是一个简简单单的单体增删改查项目呀?但事情远没有表面这么简单,诸位请接着往下看。1。1、项目的难点
  起初当我收到通知要负责这个需求时,从表面浅显的想了一下,似乎发现也不是太难,就是一个单体项目的CRUD工作,以我这手出神入化的CV大法,Hlod住它简直轻轻松松,因此当时也没想太多就直接接手了,项目初期由于团队每位成员经验都很丰富,各自凭借着个人的Copy神功,项目的开发进度可谓是一骑千里,但慢慢的问题来了,而且这个问题还不小!
  当时大概对外预计分发1000台机器,每台机器正式投入运营后,预估单日会产生500600条数据的产出,套到前面的举例中,也就是大概会向几百个超市投放共计1000台收银机,每个收银台平均下来之后,大概单日内会有500600个顾客结账!
  这里咱们做个数学题:现在有1000台机器,每台机器单日就算产生500条数据:1000500500000,这也就意味着单日的账单表中会新增50W条流水数据,单月整个账单表的数据增长量为:50W301500W!
  单月数据增长1500W的概念不言而喻,这也就代表着一年的数据增长量为1500W121。8E,这批机器投入后预估最少会运行三年起步,甚至十年乃至更久,同时第一批次就要投入1000台,后面可能还会有第二批次、第三批次。。。。。的投入。
  50W只是最低的账单流水数据量,后续正式运营后可能数据量更大,此时架构的设计就成了难题!1。2、方案的探讨
  基本上当时团队的成员中,没人在此之前碰过这类需求,因此开了一个研讨会,去决定该如何将具体的方案落地,这里有人也许会说,数据量这么大,快上分布式微服务啊!但实则解决不了这个问题,Why?因为项目整体的用户量并不大,最多同一时刻也才1000并发请求,就算这个并发量再增大几个级别,这里用单体架构优化好了也能够抗住,所以问题并不在业务系统的架构上面,而是在数据落库这方面。
  这里直接用分库可以吗?答案是也不行,Why?因为整个项目中只有账单表才有这么大的数据量,其他的用户表、系统表、功能菜单表、后台表。。。。。。,基本上不会有太大的数据量,所以直接做分库也没必要,属实有些浪费资源。
  有小伙伴可能想到了!在之前的文章中好像聊过《MySQL的表分区技术》,这里可以按月份对流水表做分区呀!乍一听似乎像那么一回事,但依旧不行,因为第一批机器投入后,单月预计就会产生1500W条数据,后续可能会增加机器数量,因此单月的数据量达到2000W、3000W。。。。。都有可能,如果按月做表分区,每个分区里面都有几千万条数据,一张账单表的流水随着时间推移,数据量甚至会达到几十亿!
  一张表中存储几十亿条数据,这基本上不现实,虽然InnoDB在数据页为16KB尺寸下,单表最多能存储64TB数据,有可能这几十亿条数据真的能存下去,但查询时的性能简直令人头大,并且最关键的是不方便后续对数据做维护、管理、备份和迁移工作。
  因此经过一番探讨后,最后决定选择了表分区技术的进阶版实现,即单库内做水平分表,按月份对数据做分表,也就是将账单表分为monthbills202210、monthbills202211、monthbills202212。。。。。。。以月份结尾的多张表,每个月的账单流水数据最终都会插入到各自的月份表中。
  最终架构定型为:业务系统使用单体架构数据库使用单库流水表按月份做水平分表。二、按月分表方案的落地实践
  在上一阶段中已经决定好了具体的方案,但又该如何将方案落地呢?首先咱们先把方案落地的思路捋清楚:能够自动按月创建一张月份账单表,从而将每月的流水数据写入进去。写入数据时,能够根据当前的日期,选择对应的月份账单表并插入数据。
  实现了上面两个需求后,整个方案近乎落地了一半,但接下来该如何去实现相应功能呢?咱们一点点来动手实现。2。1、利用存储过程实现按月动态创建表
  创建表的SQL语句大家都不陌生,按月份创建表之前,自然也需要一份原生创建表的DDL语句,如下:CREATETABLEmonthbills202211(monthbillsidint(8)NOTNULLAUTOINCREMENTCOMMENT账单ID,serialnumbervarchar(50)NOTNULLCOMMENT流水号,billsinfotextNOTNULLCOMMENT账单详情,paymoneydecimal(10,3)NOTNULLCOMMENT支付金额,machineserialnovarchar(20)NOTNULLCOMMENT收银机器,billdatetimestampNOTNULLCOMMENT账单日期,billcommentvarchar(100)NULLDEFAULT无COMMENT账单备注,PRIMARYKEY(monthbillsid)USINGBTREE,UNIQUEserialnumber(serialnumber),KEYbilldate(billdate))ENGINEInnoDBCHARACTERSETutf8COLLATEutf8generalciROWFORMATC复制代码
  上述的语句会创建一张月份账单表,这张表主要包含七个字段,如下:
  字段
  简介
  描述
  monthbillsid
  月份账单ID
  主要作为月份账单表的主键字段
  serialnumber
  流水号
  所有账单流水数据的唯一流水号
  billsinfo
  账单详情
  顾客本次订单中,购买的所有商品详情数据
  paymoney
  支付金额
  本次顾客共计消费的总金额
  machineserialno
  收银机器
  负责结算顾客订单的收银机器
  billdate
  账单日期
  本次账单的结算日期
  billcomment
  账单备注
  账单的额外备注
  其中注意的几个小细节:日期字段使用的是timestamp类型,而并非datetime,因为前者更省空间。账单详情字段用的是text类型,因为这个字段可能会出现很多的信息。定义了一个和表没有关系的自增字段作为主键,用于维护聚簇索引树的结构。
  除开有上述七个字段外,还有三个索引:
  索引字段
  索引类型
  索引作用
  monthbillsid
  主键索引
  主要作用就是用来维护聚簇索引树
  serialnumber
  唯一索引
  当需要根据流水号查询数据时使用
  billdate
  唯一联合索引
  当需要根据日期查询数据时使用
  到这里就有了最基本的建表语句,主要是用来创建第一张月份账单表,如果想要实现动态按照每月建表的话,还需要用到存储过程来实现,接着来写一个存储过程,但如若对于存储过程语法还不了解的各位小伙伴,这里就不再做基础讲解,可自行阅读之前的《全解MySQL存储过程》。
  最终撰写出的存储过程如下:DELIMITERDROPPROCEDUREIFEXISTScreatetablebymonthCREATEPROCEDUREcreatetablebymonth()BEGIN用于记录下一个月份是多久DECLAREnextMonthvarchar(20);用于记录创建表的SQL语句DECLAREcreateTableSQLvarchar(5210);执行创建表的SQL语句后,获取表的数量DECLAREtableC用于记录要生成的表名DECLAREtableNamevarchar(20);用于记录表的前缀DECLAREtableprefixvarchar(20);获取下个月的日期并赋值给nextMonth变量SELECTSUBSTR(replace(DATEADD(CURDATE(),INTERVAL1MONTH),,),1,6)INTOnextM设置表前缀变量值为定义表的名称表前缀月份,即monthbills2022112这个格式SETtableNameCONCAT(tableprefix,nextMonth);定义创建表的SQL语句setcreateTableSQLconcat(createtableifnotexists,tableName,(monthbillsidint(8)NOTNULLAUTOINCREMENTCOMMENT账单ID,serialnumbervarchar(50)NOTNULLCOMMENT流水号,billsinfotextNOTNULLCOMMENT账单详情,paymoneydecimal(10,3)NOTNULLCOMMENT支付金额,machineserialnovarchar(20)NOTNULLCOMMENT收银机器,billdatetimestampNOTNULLDEFAULTnow()COMMENT账单日期,billcommentvarchar(100)NULLDEFAULT无COMMENT账单备注,PRIMARYKEY(monthbillsid)USINGBTREE,UNIQUEserialnumber(serialnumber),KEYbilldate(billdate))ENGINEInnoDBCHARACTERSETutf8COLLATEutf8generalciROWFORMATC);使用PREPARE关键字来创建一个预备执行的SQL体PREPAREcreatestmtfromcreateTableSQL;使用EXECUTE关键字来执行上面的预备SQL体:createstmtEXECUTE释放掉前面创建的SQL体(减少内存占用)DEALLOCATEPREPARE执行完建表语句后,查询表数量并保存再tableCount变量中SELECTCOUNT(1)INTOtableCountFROMinformationschema。TABLESWHERETABLENAMEtableN查询一下对应的表是否已存在SELECTtableCounttableCEND复制代码
  上述这个存储过程比较长,但基本上都写好了注释,所以阅读起来应该还是比较轻松的,也包括该存储过程在MySQL5。1、8。0版本中都测试过,所以大家也可以直接用,主要拆解一下里面较为难理解的一句SQL,如下:SELECTSUBSTR(replace(DATEADD(CURDATE(),INTERVAL1MONTH),,),1,6)INTOnextM复制代码
  这条语句执行之后会生成一个202212这样的月份数字,主要用来作为表名的后缀,以此来区分不同的表,但里面用了几个函数组合出了该效果,下面做一下拆解,如下:在当前日期的基础上增加一个月,如2022111223:46:11,会得到2022121223:46:11selectDATEADD(CURDATE(),INTERVAL1MONTH);使用空字符代替日期中的符号,得到2022121223:46:11这样的效果selectreplace(2022121223:46:11,,);对字符串做截取,获取第一位到第六位,得到202212这样的效果selectSUBSTR(2022121223:46:11,1,6);复制代码
  经过上述拆解之后大家应该能看的很清楚,最终每次调用该存储过程时,都会基于当前数据库的时间,然后向后增加一个月,同时将格式转化为YYYYMM格式,接下来调用该存储过程,如下:callcreatetablebymonth();tableCount1复制代码
  当返回的值为1而并非0时,就表示已经在数据库中查到了前面通过存储过程创建的表,即表示动态创建表的存储过程可以生效!接着为了能够每月定时触发,可以在MySQL中注册一个每月执行一次的定时事件,如下:createEVENTcreatetablebymonthevent创建一个定时器ONSCHEDULEEVERY1MONTH每间隔一个月执行一次STARTS2022112800:00:00从2022112800:00:00后开始ONCOMPLETIONPRESERVEENABLE执行完成之后不删除定时器DOcallcreatetablebymonth();每次触发定时器时执行的语句复制代码
  MySQL5。1版本中除开引入了存储过程函数、触发器的支持外,还引入了定时器的技术,也就是支持定时执行一条SQL,此时咱们可借助MySQL自带的定时器来定时调用之前的存储过程,最终实现按月定时创建表的需求!
  但定时器在使用之前,需要先查看定时器是否开启,如下:
  
  如果是OFF关闭状态,需要通过setglobaleventscheduler1命令开启。
  如果想要永久生效,MySQL8。0以下的版本可找到my。inimy。conf文件,然后找到〔mysqld〕的区域,再里面多加入一行eventschedulerON的配置即可。
  这里再附上一些管理定时器的命令:查看创建的定时器selectfrommysql。selectfrominformationschema。EVENTS;删除一个定时器dropevent定时器名称;关闭一个定时器任务alterevent定时器名称onCOMPLETIONPRESERVEDISABLE;开启一个定时器任务alterevent定时器名称onCOMPLETIONPRESERVEENABLE;复制代码
  经过上述几步后,就能够让MySQL自己按月创建表了,但为啥我会将定时器的时间设置为2022112800:00:00这个时间后开始呢?因为202211这张表我已经手动建立了,不将建立表的工作放在月初一号执行,这是因为前面的存储过程是创建下月表,而不是创建当月表,同时月底提前创建下月表,还能提高容错率,在MySQL定时器故障的情况下,能预留人工介入的时间。2。2、写入数据时能够根据月份插入对应表
  作为一个后端项目,必然还需要搭建客户端,这里用SpringBootMyBatis来快速构建一个单体项目(最后会给出完整源码),这里需要注意,月份账单表对应的实体类中要多出一个targetTable字段,如下:publicclassMonthBills{月份账单表IDprivateIntegermonthBillsId;账单流水号privateStringserialN支付金额privateBigDecimalpayM收银机器privateStringmachineSerialNo;账单日期privateDatebillD账单详情privateStringbillsI账单备注privateStringbillC要操作的目标表privateStringtargetT省略构造方法和GetSet方法。。。。。}复制代码
  上述的实体类与之前的表字段结构几乎完全相同,但会多出一个targetTable属性,后续会用来记录要操作的目标表,接着再撰写一个工具类,如下:publicclassTableTimeUtils{使用ThreadLocal来确保线程安全,或者可以使用Java8新引入的DateTimeFormatter类:monthTL:负责将一个日期处理成YYYYMM格式privatestaticThreadLocalSimpleDateFormatmonthTLThreadLocal。withInitial(()newSimpleDateFormat(YYYYMM));表的前缀privatestaticStringtableP将一个日期格式化为YYYYMM格式publicstaticStringgetYearMonth(Datedate){returnmonthTL。get()。format(date);}获取目标数据的表名(操作单条数据公用的方法:增删改查)publicstaticvoidgetDataByTable(MonthBillsmonthBills){获取传入对象的时间DatebillDatemonthBills。getBillDate();根据该对象中的时间,计算出要操作的表名后缀StringyearMonthgetYearMonth(billDate);将表前缀和后缀拼接,得到完整的表名,如:monthbills202211monthBills。setTargetTable(tablePrefixyearMonth);}}复制代码
  这个工具类主要负责处理日期的时间格式,以及用来定位要操作的目标表名,对于日期格式化类:SimpleDateFormat由于是线程不安全的,所以使用ThreadLocal来确保线程安全!上述工具类中主要提供了两个基础方法:getYearMonth():将一个日期格式化成YYYYMM格式。getDataByTable():获取单条数据操作时的表名。
  有了工具类后,接着来撰写Dao、Mapper层的代码,如下:MapperRepositorypublicinterfaceMonthBillsMapper{intdeleteByPrimaryKey(IntegermonthBillsId);intinsertSelective(MonthBillsrecord);MonthBillsselectByPrimaryKey(IntegermonthBillsId);intupdateByPrimaryKeySelective(MonthBillsrecord);}复制代码
  上述是月份账单表对应的DaoMapper接口,因为我这里是通过MyBatis的逆向工程文件自动生成的,所以名字就是上面那样,我这边未成更改,接着来看看对应的xml文件,如下:insertidinsertSelectiveparameterTypecom。zhuzi。dbMachineSubmeter。entity。MonthBillsinsertinto{targetTable}trimprefix(suffix)suffixOverrides,iftestmonthBillsId!nullmonthbillsid,ififtestserialNumber!nullserialnumber,ififtestpayMoney!nullpaymoney,ififtestmachineSerialNo!nullmachineserialno,ififtestbillDate!nullbilldate,ififtestbillComment!nullbillcomment,ififtestbillsInfo!nullbillsinfo,iftrimtrimprefixvalues(suffix)suffixOverrides,iftestmonthBillsId!null{monthBillsId,jdbcTypeINTEGER},ififtestserialNumber!null{serialNumber,jdbcTypeVARCHAR},ififtestpayMoney!null{payMoney,jdbcTypeDECIMAL},ififtestmachineSerialNo!null{machineSerialNo,jdbcTypeVARCHAR},ififtestbillDate!null{billDate,jdbcTypeTIMESTAMP},ififtestbillComment!null{billComment,jdbcTypeVARCHAR},ififtestbillsInfo!null{billsInfo,jdbcTypeLONGVARCHAR},iftriminsert复制代码
  上述这么大一长串,其实也不是俺手敲的,依旧是MyBatis逆向工程生成的代码,但我对其中的一处稍微做了改动,如下:原本生成的代码是:insertintomonthbills202211然后被我改成了:insertinto{targetTable}复制代码
  还记得最开始的实体类中,咱们多添加的那个targetTable属性嘛?在这里会根据该字段的值动态的去操作不同月份的表,接着来写一下Service层的接口和实现类,如下:Service接口(目前里面只有一个方法)publicinterfaceIMonthBillsService{intinsert(MonthBillsmonthBills);}Service实现类ServicepublicclassMonthBillsServiceImplimplementsIMonthBillsService{AutowiredprivateMonthBillsMapperbillsMOverridepublicintinsert(MonthBillsmonthBills){获取要插入数据的表名TableTimeUtils。getDataByTable(monthBills);返回插入数据的状态returnbillsMapper。insertSelective(monthBills);}}复制代码
  在service层目前仅实现了一个插入数据的方法,其中的逻辑也非常简单,仅仅在调用Dao层的插入方法之前,获取了一下当前这条数据要插入的表名,最后来看看ControllerAPI层,如下:RestControllerRequestMapping(bills)publicclassMonthBillsAPI{AutowiredprivateIMonthBillsServicebillsS账单结算的APIRequestMapping(settleUp)publicStringsettleUp(MonthBillsmonthBills){设置账单交易时间为当前时间monthBills。setBillDate(newDate(System。currentTimeMillis()));使用UUID随机生成一个流水号monthBills。setSerialNumber(monthBills。getMachineSerialNo()System。currentTimeMillis());调用新增账单数据的service方法if(billsService。insert(monthBills)0){return账单结算成功;}return账单结算失败;}}复制代码
  在API层主要对外提供了一个账单结算的接口,这里为了方便测试,所以对于请求方式的处理就没那么严谨了,在调用该接口后,会先获取一下当前系统时间作为账单时间,接着会随机生成一个UUID作为流水号,最后就会调用service层的insert()方法。
  到这里为止就搭建出了一个最简单的WEB接口,接着来做一个小小的测试,这里为了方便就不用专门的PostMan工具了,就通过浏览器简单的调试一下,接口如下:
  http:localhost:8080billssettleUp?billsInfo白玉竹子3:9999。999payMoney9999。999machineSerialNoNF002X
  最终测试效果图如下:
  效果很明显,确实做到了咱们需要的效果,接着来看看控制台输出的SQL日志,如下:
  主要可以观察到,原本xml中的动态表名,最终会根据月份被替换为具体的表名,最后再来看看数据库中的表是否真正插入了数据,如下:
  因为之前测试过一次,因此表中早有了一条数据,主要观察第二条,的确是咱们刚刚测试时插入的数据,这也就意味着咱们按月动态插入的需求已经实现。
  但看到这里估计绝大部分小伙伴略微有些懵,毕竟一通代码下来看起来,尤其是不在IDEA工具里面,没那么方便调试,因此最后画一个执行流程图,提供给诸位来梳理整体思路!
  客户端调用结算接口,传入相关的账单数据,即账单详情、账单金额、收银机器。API层会先获取当前系统时间作为账单交易的时间,然后调用Service层的插入方法。Service层会先根据账单交易时间,获取到数据具体要插入的表名,接着调用Dao层接口。Dao层会根据上层传递过来的表名,生成具体的SQL语句,然后执行插入数据的操作。三、按月分表后要解决的问题
  上述已经将最基础的需求做了简单实现,那么接着再分析一下这些月份账单表还会有哪些需求呢?除去最基本的新增操作外,还会有删除、修改、查询账单的需求。一般账单表中的流水数据,都会支持按时间进行范围查询操作。
  上述这两个需求会是账单表中还会存在的操作,对于第一点也比较容易实现,就是要求客户端在修改、删除、查询数据时,都必须携带上对应的时间,一般客户端的修改、删除操作都是基于先查询出数据的基础之上的,而一般查询数据都会按照月份进行查询,或者根据流水号进行查询。3。1、根据流水号查询数据
  还记得前面对于流水号的设计嘛?前面没有太过说明,这里咱们单独拧出来聊一聊:setSerialNumber(monthBills。getMachineSerialNo()System。currentTimeMillis());复制代码
  这里使用了收银机器序列号时间戳作为账单流水号,因为同一台机器在同一时间内,绝对只能对一个账单进行结算,所以再结合递增的时间戳,就能够得到一个全局唯一的流水号。System。currentTimeMillis()获取到的时间戳是13位数字,会放在机器序列号的后面,那接下来如果客户端要根据流水号查询账单数据,又该如何定位具体的表呢?首先需要在工具类中撰写一个新的方法:根据流水号得到表名publicstaticvoidgetTableBySerialNumber(MonthBillsmonthBills){获取流水号的后13位(时间戳)StringtimeMillismonthBills。getSerialNumber()。substring(monthBills。getSerialNumber()。length()13);将字符串类型的时间戳转换为long类型longmillisLong。parseLong(timeMillis);调用getYearMonth()方法获取时间戳中的年月StringyearMonthgetYearMonth(newDate(millis));用表的前缀名拼接年月,得到最终要操作的表名monthBills。setTargetTable(tablePrefixyearMonth);}复制代码
  上面这个方法实际上很简单,就是先解析流水号中的时间戳,然后根据时间戳得到具体的年月,最后拼接表的前缀名,得到最终需要操作的表名,接着来写一下Dao层代码,如下:!在MonthBillsMapper中多定义一个接口:!MonthBillsselectBySerialNumber(MonthBillsrecord);!定义返回的结果集resultMapidResultMapMonthBillstypecom。zhuzi。dbMachineSubmeter。entity。MonthBillsconstructoridArgcolumnmonthbillsidjdbcTypeINTEGERjavaTypejava。lang。IntegerconstructorresultMap!定义字段列表sqlidBaseColumnListmonthbillsid,serialnumber,billsinfo,paymoney,machineserialno,billdate,billcommentsql!编写对应的查询语句,这里依旧是通过{targetTable}动态表名做查询selectidselectBySerialNumberresultMapResultMapMonthBillsparameterTypecom。zhuzi。dbMachineSubmeter。entity。MonthBillsselectincluderefidBaseColumnListfrom{targetTable}whereserialnumber{serialnumber,jdbcTypeVARCHAR}select复制代码
  接着来写一下Service层的代码,如下:在IMonthBillsService接口中多定义一个方法MonthBillsselectBySerialNumber(MonthBillsmonthBills);在MonthBillsServiceImpl实现类中撰写具体的实现OverridepublicMonthBillsselectBySerialNumber(MonthBillsmonthBills){根据流水号获取要查询数据的具体表名TableTimeUtils。getTableBySerialNumber(monthBills);调用Dao层根据流水号查询数据的方法returnbillsMapper。selectBySerialNumber(monthBills);}复制代码
  这里的实现尤为简单,仅调用了一下前面写的工具类方法,获取了一下要查询数据的动态表名,接着再来写一下API层的接口,如下:根据流水号查询数据的APIRequestMapping(selectBySerialNumber)publicStringselectBySerialNumber(MonthBillsmonthBills){调用Service层根据流水号查询数据的方法MonthBillsresultbillsService。selectBySerialNumber(monthBills);if(result!null){returnresult。toString();}return未查询到流水号对应的数据;}复制代码
  接着来做一下测试,调用地址如下:http:localhost:8080billsselectBySerialNumber?serialNumberNF002X1668494222684
  测试效果图如下:
  此时会发现,根据流水号查询数据的效果就实现啦,这里主要是得设计好流水号的组成,其中一定要包含一个时间戳在内,这样就能够通过解析流水号的方式,得到具体要查询数据的表名,否则根据流水号查询数据的动作将异乎寻常的困难,因为需要把全部表扫描一次才能得到数据。
  设计好根据流水号查询数据后,对于修改和删除的操作则不再重复撰写啦!因为过程也大致相同,就是在修改、删除时,同样先根据流水号定位到具体要操作的表,接着再去对应表中做相应操作即可。3。2、按时间范围查询数据
  按时间范围查询账单的流水数据,这是所有后台管理系统中都支持的功能,在这个项目中也不例外,但想要实现这个功能,则必须要有先实现两个功能:能够根据用户输入的两个时间范围,得到两个日期之间的所有表名。能够根据第步中得到的表名,生成对应的查询语句,能够在单张表、多张表中通用。
  上述这两个需求实际上实现起来也并不难,接着来一起做一下!3。2。1、得到两个日期之间的所有表名
  想要实现这个功能,那必然需要再在工具类中撰写一个方法,如下:获取按时间范围查询时,两个日期之间,所有月份账单表的表名publicstaticListStringgetRangeQueryByTables(StringstartTime,StringendTime){声明一个日期格式化类SimpleDateFormatsdfnewSimpleDateFormat(yyyyMM);声明保存表名的集合ListStringtablesnewArrayList();try{将两个传入的字符日期转换成日期类型DatestartDatesdf。parse(startTime);DateendDatesdf。parse(endTime);用Calendar进行日期比较判断CalendarcalendarCalendar。getInstance();while(startDate。getTime()endDate。getTime()){把生成的月份拼接表前缀名,加入到集合中tables。add(tablePrefixmonthTL。get()。format(startDate));设置日期,并把比对器的日期增加一月calendar。setTime(startDate);calendar。add(Calendar。MONTH,1);获取增加后的日期startDatecalendar。getTime();}}catch(ParseExceptione){e。printStackTrace();}返回两个日期之间的所有表名}复制代码
  该方法需要传入两个参数,即两个字符串类型的时间,接着会通过Calendar工具类,对两个日期的大小做判断,当开始日期小于结束日期时,则会直接将表前缀名与年月拼接,得到一张月份账单表的表名,接着会对开始日期加一个月,然后继续重复上一步。。。。。。,直至得到两日期之间的所有表名。3。2。2、根据表名集合生成对应的SQL语句
  想要实现这个功能其实也非常简单,只需要做一堆判断即可,再在工具类中写一个方法:根据日期生成SQL语句的方法publicstaticStringgetRangeQuerySQL(StringstartTime,StringendTime){先获取两个日期之间的所有表名ListStringtablesgetRangeQueryByTables(startTime,endTime);提前创建一个字符串对象保存SQL语句StringBuffersqlnewStringBuffer();如果查询的两个日期是同一张表,则直接生成BETWEENAND的SQL语句if(tables。size()1){sql。append(selectfrom)。append(tables。get(0))。append(wherebilldateBETWEEN)。append(startTime)。append(AND)。append(endTime)。append(;);如果本次范围查询的两个日期之间有多张表}else{则用for循环遍历所有表名for(Stringtable:tables){对于第一张表则只需要查询开始日期之后的数据if(table。equals(tables。get(0))){sql。append(selectfrom)。append(table)。append(wherebilldate)。append(startTime)。append(unionall);}对于最后一张表只需要查询结束日期之前的数据elseif(table。equals(tables。get(tables。size()1))){sql。append(selectfrom)。append(table)。append(wherebilldate)。append(endTime)。append(;);对于其他表则获取所有数据}else{sql。append(selectfrom)。append(table)。append(unionall);}}}返回最终生成的SQL语句returnsql。toString();}复制代码
  这个方法看起来似乎有些长,但其实功能也非常简单,如下:如果两个日期在一个月内,则生成BETWEENAND的查询语句。如果两个日期间隔了多月,则用for循环遍历前面得到的表名:如果是第一张表,则只需要查询开始日期之后的数据,再用unionall拼接后面的语句。如果是最后一张表,则只需要查询结束日期之前的数据,以;分号结尾即可。如果是中间的表,则查询对应的所有数据,接着继续用unionall拼接其他语句。
  接着做个简单的小测试,效果如下:
  很明显,通过这两个方法,可以实现最初咱们提出的两个需求,实现这两个基础功能后,接着套入到前面的项目中3。2。3、实现按时间做范围查询的API接口
  依旧按照之前的步骤,先定义Dao层的接口和。xml文件,如下:定义一个返回多条数据的接口ListMonthBillsrangeQueryByDate(Param(sql)Stringsql);复制代码selectidrangeQueryByDateresultMapResultMapMonthBillsparameterTypejava。lang。String{sql}select复制代码
  主要观察xml文件中的代码,因为这里需要实现自定义SQL的执行,所以将SQL语句的生成工作放在了外部完成,在xml中仅需将对应的SQL语句发给MySQL执行,并接收返回结果即可,接着来写一下Service层的接口和实现:在IMonthBillsService接口中多定义一个方法ListMonthBillsrangeQueryByDate(StringstartTime,StringendTime);在MonthBillsServiceImpl实现类中撰写具体的实现OverridepublicListMonthBillsrangeQueryByDate(StringstartTime,StringendTime){获取范围查询时的SQL语句StringsqlTableTimeUtils。getRangeQuerySQL(startTime,endTime);returnbillsMapper。rangeQueryByDate(sql);}复制代码
  其实核心工作已经在之前的工具类中完成了,这里仅需调用工具类中,生成两个日期之间的查询语句即可,接着再写一下API层的对外接口,就大功告成啦!如下:按照范围查询两个日期之间的所有账单数据RequestMapping(rangeQueryByTime)publicStringrangeQueryByTime(RequestParam(start)Stringstart,RequestParam(end)Stringend){调用Service层根据流水号查询数据的方法ListMonthBillsbillsbillsService。rangeQueryByDate(start,end);if(bills!null){returnbills。toString();}return指定的日期中没有账单数据;}复制代码
  在这里面仅仅只是调用了Service层的方法而已,接下来测试一下,测试地址为:localhost:8080billsrangeQueryByTime?start20221101end20221130
  最终效果如下:
  因为我表中就两条数据,所以就做了一个单月表的测试,这里单月账单表的数据查询无误,大家也可以再建立一张其他月份的账单表,效果也是照样没有问题的3。2。4、按时间范围查询数据小结
  其实这里的做法仅仅只是为了给大家演示效果,之前的实际业务中远比这更加复杂,因为每张月份账单表会有上千万条数据,不可能一次性查询几张、几十张的月份账单表,这样对于网络、资源的开销太大。
  实际业务中,一方面会限制查询的日期范围,最多只允许客户查询近六月的账单流水。另一方面还会结合数据分页,也就是每页仅显示20条数据,随着用户的翻页动作触发后,才会对每张不同的月份账单表做查询。
  对于这种会批量查询所有账单表的业务,基本上是查询一些流水交易金额的统计数据,而且也仅是提供给后台系统操作,用于定时跑批去生成统计数据,如近一周、一月、一季、半年、一年的交易金额、账单总量。。。。。等这类需求。
  这里给大家实现这个需求的目的在于:让大家理解按月做了水平分表后,该如何查询多张表的数据。四、库内分表篇总结
  看到这里,对于库内分表篇的内容也接近了尾声,有小伙伴也许会疑惑:那如果我每月的数据量更大怎么办呢?比如前面的例子中,如果再投入了多批机器怎么办?每月的数据量达到3000W、6000W。。。。。甚至上亿怎么办?
  如若你存在这块的顾虑,其实大可不必担心,因为咱们既然可以按月分表,那能否按半月为周期分表呢?能否按星期分表呢?能否以三天、一天为一个维度分表呢?答案显然是可以的,所以数据量无论有多大,都可能按不同的周期来划分表。
  不过一般对于库内分表的场景会很少用到,毕竟库中只有某些表的数据量较大时,才会选用这种方案,如果整库的数据量较大、访问压力较高,则会直接采用分库方案(不过本篇的内容,对于一些身处东南亚的朋友,应该用的还是比较频繁的)。
  本章内容属于大家几乎很少见到的库内分表落地实践,其他一些讲解分表技术的资料文章,基本上仅停留在概念层次的分享,所以如若你看完后对你有些许收获,那请不要忘了点赞支持哦,最后也附上项目的《源码地址》,感兴趣的也可自己down下来玩一玩(里面也附带了对应的SQL代码)!
  其实库内分表除开本文讲解的方式外,大家通过整合ShardingJDBC框架来实现会更加轻松,但那样会导致依赖变多,所以如果你项目中不需要用到太多的分表,则可采用本文这种方式实现。《源码地址》:https:github。comliuhaijieAdmindbmachinesubmeter
  原文链接:https:juejin。cnpost7169098754719481864
投诉 评论 转载

江诗丹顿日内瓦上海双城记灰紫色真丝乔其纱,由层层叠叠的褶皱勾勒而成,裙摆如瀑布般流泻而下,一件优雅的高级定制礼服出现在上海一座石库门老建筑内。这里是上海张园西区16号,瑞士高级制表品牌江诗丹顿打……太阳系中第二大的土星,不但不土,还非常美丽土星和名字不同,土星不但完全不土,还非常美丽。它的体积是太阳系中第二大的,质量却很轻。轻盈的土星周围飘浮着许多旋涡状的漂亮云团,还环绕着一圈钻石一样的项链土星环。土星环是……冬天小孩洗澡为什么频繁出事?如何防范?入冬了,冷空气来临,帮小孩洗澡真是个头疼的问题。如果帮小孩洗澡分段位,你是哪个段位的?青铜:冲过即洗过,孩子完全不配合,能冲一下不错了。白银:泡到皮肤起锁,孩……湘江新区请全世界来打卡网上传说,每逢节假日,全国有一半人来了长沙,其中又有一半人在新区,还有一半人正在来新区的路上。他们为可爱的湘江新区而来,我今天为湘江新区文旅打call。新区文旅,新……OPPO大数据诊断平台设计与实践01背景随着欧加集团大数据业务的发展,现阶段公司大数据平台20个组件,1EB级别数据量,平台1000人均日活,服务已经有相当大的规模。在这样的业务背景下,越来越多的用户在……考辛斯渴望能重回勇士弥补遗憾,库里有我辅佐能打爆任何球队如今NBA球员考辛斯依然还在求职的道路上,尽管有消息证明他可能会去台湾打球,但是目前他的第一选择依然是留在NBA当中效力,毕竟这里是他的故土,并且在NBA就算拿一份底薪,那么也……东体记者富兰克林中秋后可归队比斯利15号入境ampampam直播吧9月9日讯今日,《东方体育日报》记者薛思佳在个人微博上更新了上海男篮外援最新动态和内部教学赛的最新情况。微博这样写道:富兰克林已经在上海隔离五、六天了,基本上中秋假……最近闻到了小牛的味道2022年中国股市总体来说用一个惨字形容一点也不为过,全年仅剩两个多月,中国股市难道可以由熊变牛?估计绝大多数朋友认为不跌就烧高香了,此篇文章是不是有点华众取宠,我上一篇文章写……如果再买洗衣机,一定4不买,不是执拗,是换过2台的经验家居编辑:美家指南作为大家电之一,洗衣机是否实用直接影响着居家的体验,所以在挑选的时候一定要慎重。以我家的洗衣机为例,自从搬进新家后换了已经2台了,终于搞明白了什么样的洗……MySQL分表篇该如何将月增上亿条数据的单表处理方案优雅落地分库分表实战内容基本上很少有人去分享,在网上能够搜出来的也大多属于一些方法论,但大部分技术开发真正缺少的恰恰是这些实操经验,所以后续的内容多以实践为主,携手诸位真正彻底悟透分库……18GB大内存?中兴Axon40Ultra航天版即将发布iP文名动科技在之前智能手机刚起步的阶段,中兴手机还是挺火的,但由于后来创新出现了乏力,导致逐渐被友商超过,目前还在Other阵营中徘徊。不过就产品而言,我觉得中兴手机并不差……耳林十五道沟作者:耳林望天鹅风景区是国家AAAA级旅游区,现已被国务院批准为鸭绿江上游国家级自然保护区,且正在申报中国吉林长白山望天鹅火山国家地质公园。望天鹅风景区位于长白山腹……
天价周边游普涨几倍民宿均价上千,住五天比出国玩一趟还贵希望国家给全职妈妈发工资你常欣赏鼓励你的孩子吗?那就给他(她)加三分钟朗读小s与二女儿首次同台,孩子身高远超妈妈,听到女儿美貌碾压自己暗黑破坏神不朽剧本杀即将来袭!勇士们,来一场线下冒险吧下午13点!前恒大外援曝出争议猛料,中超成国际笑话,球迷骂声普京签总统令禁止西方投资者出售在俄电力资产为争取参加内地综艺节目,不惜被长期隔离,刘恺威现状令人唏嘘看身体轮廓决定怎么穿!日本骨架分析,找出最适合自己的穿搭跌下神坛的英国传奇巨星,最低调的中场大师保罗斯科尔斯贝弗利怼记者数据表明我会投三分,所以我不想去空切诚意满满,用料十足的全景声影院功放,你心动了吗?
微波炉和烤箱可以叠放吗微波炉和烤箱是否可以叠放家有儿女有多土豪?饭菜和刘梅的化妆品,很多人现在都买不起职场加薪的5个潜规则逐个数工作没劲加薪无望如何走出倦怠沼泽酒精中毒有哪些症状及如何急救友谊在我的记忆深处幼儿国成语故事高晓攀嗨!自在生活上线聊养生谈心境尽显通透豁达彼岸花学校停课通知家乡的四季小学五年级书的作文400字热文聚热点网 深圳国际仲裁院收费管理办法是什么

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找江西南阳嘉兴昆明铜陵滨州广东西昌常德梅州兰州阳江运城金华广西萍乡大理重庆诸暨泉州安庆南充武汉辽宁