1.高并发定义
高并发:系统在单位时间内承受大量用户请求的能力【一般衡量高并发的指标就有QPS】
QPS:【Queries Per Second每秒查询次数】用来判断并发量的高低。
不同公司中对Git的使用分支命名规范也略有差异,不过整体都会分为;上线
、预发
、开发
、测试
,这样几个分支。如图是一种比较简单使用的拉取分支方式。
2024/10/11/xfg-xxx
使用带有斜线的分支命名会自动创建文件夹,对于多人开发的项目,可以直接归档。fix-用户名缩写-具体功能
保持一个标准的统一的规范提交代码,在后续的评审、检查、合并,都会非常容易处理。
1 | # 主要type |
在公司中很多时候是大家一起在一个工程开发代码,那么这个时候就会涉及合并代码的。如果有多人共同开发一个接口方法,就会在合并的时候产生冲突。所以要特别注意。
如果出现了合并代码冲突后,丢失了代码,那么这个时候一般要进行回滚操作,重新合并。
虽然 Git 提供了回滚代码的功能,但一定要谨慎使用。怎么谨慎?第一个谨慎就是 push 的代码一定确保可以构建和运行,否则不要 push!第二个谨慎是要回滚代码,需要和团队中对应的伙伴打招呼,避免影响别人测试或者上线。
fast-forward
因为此时本地分支落后于远程分支。git push origin HEAD --force
进行强制提交。或者你可以把 test 的远程分支删掉,之后在提交。只把我修改的部分合并上去,使用cheery pick
在开发Web应用时,我们经常需要处理大量的数据展示,而分页功能几乎成了标配。它不仅提升了用户体验,还减轻了服务器的负担。今天,咱们就来聊聊一个在Java圈里非常流行的分页插件——PageHelper,看看它是如何在不动声色间帮我们搞定分页难题的。
PageHelper是MyBatis的一个分页插件,它能够在不修改原有查询语句的基础上,自动实现分页功能。
简单来说,就是你在查询数据库时,告诉PageHelper你想看第几页、每页多少条数据,它就会帮你把结果集“裁剪”好。
它首先会作为一个拦截器注册到MyBatis的执行流程中。这个拦截器就像是个守门人,会在SQL语句执行前和执行后进行一些“小动作”。
拦截到SQL语句后,PageHelper并不会直接修改你的原始SQL,而是通过动态生成一段分页SQL来实现分页功能。这个过程大致如下:
LIMIT
和OFFSET
(或者数据库特定的分页语法,比如MySQL的LIMIT
,Oracle的ROWNUM
等),从而实现对结果集的裁剪。【如果要查询第20-30行数据。 limit 19,10 或者 limit 10 offset 19】具体操作网页:如何使用分页插件
PageHelper.startPage(pageNum, pageSize)
,其中pageNum
是页码,pageSize
是每页数量。后台分布式架构形形色色,特别是微服务和云原生的兴起,诞生了一批批经典的分布式架构,然而在公司内部,或者其他大型互联网企业,都是抛出自己的架构,从接入层Controller,逻辑层Service,数据层Mapper都各有特点,但这些系统设计中到底是出于何种考量,有没有一些参考的脉络呢,本文将从云原生和微服务,有状态服务,无状态服务以及分布式系统等维度探讨这些脉络。
定义:《Designing Data-Intensive Application》指出分布式系统:通过网络进行通信的多台机器的系统
好处:
【DDIA这本书主要是基于有数据有状态来讨论分布式。】
但是,现实的实践中,分布式系统存在①有状态和②无状态:
AO:【微服务的顶层】封装应用程序的业务逻辑和处理流程;负责处理用户请求,调用相关的原子服务来完成特定任务;与其他对象进行交互,协调不同的功能模块。
BO:【微服务中相关的原子服务】,负责业务原子化的服务[特定业务/数据打交道];通常被各种AO服务调用
实现有状态的分布式系统,通常有以下三种:
应用程序作为一个整体进行开发,测试和部署:
优点:
缺点:
SOA架构关注于改变IT服务在企业范围内的工作方式,定义一种可通过服务接口复用软件组件并实现其互操作的方法。
优点:
缺点:
【SOA架构的一种变体】微服务架构是一种云原生架构常用的实现方式—更强调基于云原生,独立部署,Devops,持续交付。
优点:
可扩展性和灵活性:SOA 架构将系统拆分成独立的服务,可以按需组合和重组这些服务,从而实现系统的快速扩展和灵活部署。
提高系统的可重用性:每个服务都是独立的功能单元,可以在不同的系统中复用,提高了系统的开发效率和维护成本。
降低系统的耦合性:SOA 架构通过服务之间的松耦合关系,降低了服务之间的依赖性,有利于系统的模块化和维护。
提高系统的稳定性和可靠性:SOA 架构采用了服务注册与发现机制、负载均衡、故障恢复等机制,提高了系统的稳定性和可靠性
【基于SOA架构新增的优点】
独立性:每个服务可以独立部署和更新,提高了系统的灵活性和可靠性。
可扩展性:根据需求,可以独立扩展单个服务,而不是整个应用程序。
容错性:单个服务的故障不会影响其他服务,提高了系统的稳定性。
缺点:
【基于SOA架构,主要在运维和部署上增加了难度!!!】
需要处理的问题:
而整体解决微服务问题的思路:
我直接关闭了我提交博客的git窗口,导致下次hexo d的时候提示:
关于创建xx数据库表,主键id的取值问题:如果自增可能会出现分库分表的麻烦,但是分库分表如果使用分布式id也有对应的缺点。因此,本文从①不分库分表②分库分表两个方面考虑
==每个数据库设置①不同的初始值和②相同的自增步长==
如图所示,这样可以保证DB123生成的ID是不冲突的,但是如果扩容,DB4数据库的话就没有初始值。
因此解决方案:
①根据扩容考虑决定步长,可以让多个数据库之间有空隙数字,可以扩容
②在其他未标记去扩容
==其实就是给数据库一批ID,不管多个DB之间的是否联系和连续,可能会出现多个数据库内连续,外不连续==
方案一步长的问题不好考虑,那我干脆一台机器分配,我分配的话肯定不会出现没法扩充,只是没办法保证多个数据库之间的ID是连续的。我DB4数据库来了,我可能忘了我就给他500-599的。
形式:32个十六进制数字一共是128位【8-4-4-4-12】
优点:不是有序的,安全性更高
缺点:
①不是有序的,所以做主键的话innodb聚集索引内存消耗大,读写效率低;
②32个数字长度大,导致innodb叶子节点存储过大;
③因为无序,查找效率低下
第1个bit位:保留位,无实际作用
第2-42的bit位:这41位表示时间戳,精确到毫秒级别
第43-52的bit位:这10位表示专门负责生产ID的工作机器的id
第53-64的bit位:这12位表示序列号,也就是1毫秒内可以生成2 12 2^{12}2
优点:
①整体上按照时间趋势增加,后续插入索引树的性能较好;
②整个分布式系统不会发生ID碰撞;
③本地生成,且不依赖数据库,没有网络消耗
缺点:
①强依赖时间容易发生时种回拨【Map存储<机器ID,max_id>服务器出故障就从max_id重新生成】
目前,我们的定时任务都是基于SpringTask来实现的。但是SpringTask存在一些问题:
不仅仅是SpringTask,其它单机使用的定时任务工具,都无法实现像这种任务执行者的调度、任务执行顺序的编排、任务监控等功能。这些功能必须要用到分布式任务调度组件。
我们先来看看普通定时任务的实现原理,一般定时任务中会有两个组件:
因此在多实例部署的时候,每个启动的服务实例都会有自己的任务触发器,这样就会导致各个实例各自运行,无法统一控制:
如果我们想统一控制各服务实例的任务执行和调度—>统一控制[统一出发、统一调度]
事实上,大多数的分布式任务调度组件都是这样做的:
这样一来,具体哪个任务该执行,什么时候执行,交给哪个应用实例来执行,全部都有统一的任务调度服务来统一控制。并且执行过程中的任务结果还可以通过回调接口返回,让我们方便的查看任务执行状态、执行日志。这样的服务就是分布式调度服务
能够实现分布式任务调度的技术有很多,常见的有:【越往右越牛逼】
Quartz | XXL-Job | SchedulerX | PowerJob | |
---|---|---|---|---|
定时类型 | CRON | 频率、间隔、CRON | 频率、间隔、CRON、OpenAPI | 频率、间隔、CRON、OpenAPI |
任务类型 | Java | 多语言脚本 | 多语言脚本 | 多语言脚本 |
任务调度方式 | 随机 | 单机、分片 | 单机、广播、Map、MapReduce | 单机、广播、分片、Map、MapReduce |
管理控制台 | 无 | 支持 | 支持 | 支持 |
日志白屏 | 无 | 支持 | 支持 | 支持 |
报警监控 | 无 | 支持 | 支持 | 支持 |
工作流 | 无 | 有限 | 支持 | 支持 |
其中:
XXL-JOB的运行原理和架构如图:
XXL-JOB分为两部分:
其中,我们可以打开xxl-job页面:
自己部署,分为两步:
sql语句在对应github文件夹下:
docker命令:
1 | docker run \ |
每10s打印一次hello…
1 | <!--xxl-job--> |
1 | - adminAddress:调度中心地址,天机学堂中就是填虚拟机地址 |
JobHandler一定要和方法@XXlJob内容一致
【想要测试的话也可以手动执行一次任务,但是要设置好调度策略】
也可以在页面看到日志:
刚才定义的定时持久化任务,通过while死循环,不停的查询数据,直到把所有数据都持久化为止。这样如果数据量达到数百万,交给一个任务执行器来处理会耗费非常多时间—->实例多个部署,这样就会有多个执行器并行执行(但是多个执行器执行相同代码,都从第一页开始也会重复处理)—->任务分片【分片查询】
举例[类似于发牌]:
最终,每个执行器处理的数据页情况:
要想知道每一个执行器执行哪些页数据,只要弄清楚两个关键参数即可:
因此,现在的关键就是获取两个数据:
这两个参数XXL-JOB作为任务调度中心,肯定是知道的,而且也提供了API帮助我们获取:
这里的分片序号其实就是执行器序号,不过是从0开始,那我们只要对序号+1,就可以作为起始页码了
根据实际情况,分成多个机器[这个用例,分片1,2,3;步长为1]
使用xxl-job定时每月初进行持久化:
①根据计算上个月时间创建上赛季mysql表
②根据查询出来上赛季redis数据,数据库新表名通过mp动态表名插件(本质是一个拦截器,在与mapper数据库接触过程中通过threadlocal更改数据库名)】然后查询数据
③根据非阻塞语句del删除redis上赛季数据—但是我考虑使用分片,这样导致分片1执行完异步执行删除,但是分片2执行完数据好像又回来了【针对②查询结果分页用xxlJob分片,log查日志没解决,我就打断点发现是分片次数问题,我就redis添加一个总数,一个分片执行次数,然后将删除逻辑放在一个新的定时任务,判断总数==分片执行次数,符合的情况才删除】
表分区(Partition)是一种数据存储方案,可以解决单表数据较多的问题【MySQL5.1开始支持表分区功能】
如果表数据过多 —> 文件体积非常大 —> 文件跨越多个磁盘分区 —> 数据检索时的速度就会非常慢 —>【Mysql5.1引入表分区】按照某种规则,把表数据对应的ibd文件拆分成多个文件来存储。
从物理上来看,一张表的数据被拆到多个表文件存储了【多张表】
从逻辑上来看,他们对外表现是一张表【一张表】 — CRUD不会变化,只是底层MySQL处理上会有变更,检索时可以只检索某个文件就可以
例如,我们的历史榜单数据,可以按照赛季切分:
此时,赛季榜单表的磁盘文件就被分成了两个文件,但逻辑上还是一张表。CRUD不会变化,只是底层MySQL处理上会有变更,检索时可以只检索某个文件就可以
表分区的好处:
1.可以存储更多的数据,突破单表上限。甚至可以存储到不同磁盘,突破磁盘上限
2.查询时可以根据规则只检索某一个文件,提高查询效率
3.数据统计时,可以多文件并行统计,最后汇总结果,提高统计效率【分而治之,各自统计】
4.对于一些历史数据,如果不需要时,可以直接删除分区文件,提高删除效率
表分区的方式:【对数据做水平拆分】
开发者自己对表的处理,与数据库无关
从物理上来看,一张表的数据被拆到多个表文件存储了【多张表】
从逻辑上来看,【多张表】 — CRUD会变化,需要考虑取哪张表做数据处理
在开发中我们很多情况下业务需求复杂,更看重分表的灵活性。因此,我们大多数情况下都会选择分表方案。
分表的好处:
1.拆分方式更加灵活【可以水平也可以垂直】
2.可以解决单表字段过多问题【垂直分表,分在多个表】
分表的坏处:
例如,对于赛季榜单,我们可以按照赛季拆分为多张表,每一个赛季一张新的表。如图:
这种方式就是水平分表,表结构不变,仅仅是每张表数据不同。查询赛季1,就找第一张表。查询赛季2,就找第二张表。
如果一张表的字段非常多(比如达到30个以上,这样的表我们称为宽表)。宽表由于字段太多,单行数据体积就会非常大,虽然数据不多,但可能表体积也会非常大!从而影响查询效率。
例如一个用户信息表,除了用户基本信息,还包含很多其它功能信息:
无论是分区,还是分表,我们刚才的分析都是建立在单个数据库的基础上。但是单个数据库也存在一些问题:
综上,在大型系统中,我们除了要做①分表、还需要对数据做②分库—>建立综合集群。
优点:【解决了单个数据库的三大问题】
1.解决了海量数据存储问题,突破了单机存储瓶颈
2.提高了并发能力,突破了单机性能瓶颈
3.避免了单点故障
缺点:
1.成本非常高【要多个服务器,多个数据库】
2.数据聚合统计比较麻烦【因为牵扯多个数据库,有些语句会很麻烦】
3.主从同步的一致性问题【主数据库往从数据库更新,会有不可取消的延误时间,只能通过提高主从数据库网络带宽,机器性能等操作(↓)延误时间】
4.分布式事务问题【因为涉及多个数据库多个表,使用seata分布式事务可以解决】
微服务项目中,我们会按照项目模块,每个微服务使用独立的数据库,因此每个库的表是不同的
[保证单节点的高可用性]给数据库建立主从集群,主节点向从节点同步数据,两者结构一样
为了激励东林学子学习,可以设定一个学习积分的排行榜。优秀的学子可以给予优惠券
这个页面信息比较密集,从上往下来看分三部分:
签到最核心的包含两个要素:
同时要考虑一些功能要素,比如:
1 | CREATE TABLE `sign_record` ( |
无【基于redis做的,没有mysql数据库表】
回到个人积分页面,在页面中部有一个签到表:
可以看到这就是一个日历,对应了每一天的签到情况。日历中当天的日期会高亮显示为《打卡》状态,点击即可完成当日打卡,服务端自然要记录打卡情况。
因此这里就有一个接口需要实现:①签到接口
除此以外,可以看到本月第一天到今天为止的所有打卡日期也都高亮显示标记出来了。也就是说页面还需要知道本月到今天为止每一天的打卡情况。这样对于了一个接口:②查询本月签到记录
Redis 的 Bitmap(位图)是一种特殊的字符串数据类型,它利用字符串类型键(key)来存储一系列连续的二进制位(bits),每个位可以独立地表示一个布尔值(0 或 1)。这种数据结构非常适合用于存储和操作大量二值状态的数据,尤其在需要高效空间利用率和特定位操作场景中表现出色。
我们可以使用setbit getbit bitcount bitfield四个指令:
1 | # 签到/取消签到【给某人的某某年某某月为一个位图】 0就是偏移量【第一天】 1【1就是签到/0是不签到】 |
基础类型:Redis最基础的数据类型只有5种:String、List、Set、SortedSet、Hash【其它特殊数据结构大多都是基于以上5这种数据类型】
BitMap基于String结构【String类型底层是SDS,会有一个字节数组用来保存数据。而Redis就提供了几个按位操作这个数组中数据的命令,实现了BitMap效果】
由于String类型的最大空间是512MB=2的31次幂个bit,因此可以存储的数据量级很大!!!【一个月才是31bit,四个字节】–> ==bitMap扩容是8个字节一组==
在个人中心的积分页面,用户每天都可以签到一次,连续签到则有积分奖励,请实现签到接口,记录用户每天签到信息,方便做签到统计。
在个人中心的积分页面,用户每天都可以签到一次:
而在后台,要做的事情就是把BitMap中的与签到日期对应的bit位,设置为1
mysql设计:【占用空间大】
我们设计了签到功能对应的数据库表:sign_record[主键id,用户id,签到年月日,是否可以补签]。这张表的一条记录就是一个用户一次的登录记录。如果一个用户一年签到100次,那就是100条记录,如果有100w用户,就会产生一亿条记录。—->占用空间会越来越大
Redis设计:【只需要存储一个用户是否签到,0未签到,1签到】—>使用bitMap
如果我们按月来统计用户签到信息,签到记为1,未签到记为0,就可以用一个长度为31位的二进制数来表示一个用户一个月的签到情况。最终效果如下:
我们知道二进制是计算机底层最基础的存储方式了,其中的每一位数字就是计算机信息量的最小单位了,称之为bit,一个月最多也就 31 天,因此一个月的签到记录最多也就使用 31 bit 就能保存了,还不到 4 个字节【mysql数据库就要使用数百字节】
无
考虑签到只需要1/0,那就使用bitMap;然后YYMM:Userid就是一个bitMap【代表某人某年某月的登录】,一共设计31个bit位就可以代表一个月的签到数据;扩容的话是8位一组,一般一个月就是4组32位(最后一位暂时没有用)
连续登录天数:从当前天从后往前算连续1的个数【一定是从后往前】;从后往前就用算出来的十进制数&1做与运算【只关心最后一位结果】,然后右移十进制得到前面的一位
1 | int count = 0; // 定义一个计数器 |
问题三:怎么判断重复签到?
利用setbit返回值的特性
问题四:bitmap用哪些指令了?
在签到日历中,需要把本月(第一天-今天)的所有签到过的日期高亮显示。
因此我们必须把签到记录返回,具体来说就是每一天是否签到的数据。是否签到,就是0或1,刚好在前端0和1代表false和true,也就是签到或没签到。
因此,每一天的签到结果就是一个0或1的数字,我们最终返回的结果是一个0或1组成的数组,对应从本月第1天到今天为止每一天的签到情况。
综上,最终的接口如下:
无
问题一:如何获取本月的登录记录?
根据bitfield指令可以获得本月等登录记录(0001…0111001)的十进制数字
问题二:如何转为二进制,并且统计转为byte数组?
第一种办法,十进制转为字符串二进制,然后二进制char的for循环遍历得到byte[①必须-‘0’才是数字1,不然是ascii码的48;②因为十进制不是32位,转出来也不是32位!!!]
第二种办法,按照统计连续天数的思路(10进制与1进行与运算,可以依次倒序取出所有的0/1,然后逆序一下就是结果)
具体的积分获取细则如下:
1 | 1. 签到规则 |
用户获取积分的途径有5种:
这个页面信息比较密集,从上往下来看分三部分:
积分记录的目的有两个:一个是统计用户当日某一种方式获取的积分是否达到上限;一个是统计积分排行榜。
要达成上述目的我们至少要记录下列信息:
1 | CREATE TABLE IF NOT EXISTS `points_record` ( |
针对数据库的积分类型字段:设计成枚举类型
由积分规则可知,获取积分的行为多种多样,而且每一种行为都有自己的独立业务。而这些行为产生的时候需要保存一条积分明细到数据库。
我们显然不能要求其它业务的开发者在开发时帮我们新增一条积分记录,这样会导致原有业务与积分业务耦合。因此必须采用异步方式,将原有业务与积分业务解耦。如果有必要,甚至可以将积分业务抽离,作为独立微服务。
因此,我们需要为每一种积分行为定义一个不同的RoutingKey【用来分辨不同的业务,从而进行不同的业务处理】
==获取到积分,发送MQ给积分微服务就行【加不加积分微服务自己负责】==
==其他微服务学习获得积分(用户id,学习到的积分)—》积分微服务【内部判断是否上限,未上限的情况下加入到积分表】==
MQ接受消息
积分微服务处理业务
问题一:MQ发送什么消息?怎么判断是啥业务?
签到,评论,点赞等操作都可以获得积分,然后可以通过MQ异步进行更新;只需要用户id和获得积分数就可以【加不加的上是积分微服务负责,不同交换机代表不同获取积分的业务】
问题二:怎么判断今日积分是否超标?–使用sum函数统计
问题三:积分怎么计算的?
在个人中心,用户可以查看当天各种不同类型的已获得的积分和积分上限:
可以看到,页面需要的数据:
而且积分类型不止一个,所以结果应该是集合。
就是根据数据group by type分别取出类型和对应的sum(points)
另外,这个请求是查询当前用户的积分信息,所以只需要知道当前用户即可, 无需传参。
综上,接口信息如下:
顶部展示的当前用户在榜单中的信息,其实也属于排行榜信息的一部分。因为排行榜查出来了,当前用户是第几名,积了多少分也就知道了。
当我们点击更多时,会进入历史榜单页面:
排行榜是分赛季的,而且页面也需要查询到历史赛季的列表。因此赛季也是一个实体,用来记录每一个赛季的信息。当然赛季信息非常简单:
排行榜也不复杂,核心要素包括:
当然,由于要区分赛季,还应该关联赛季信息:
1 | CREATE TABLE IF NOT EXISTS `points_board_season` ( |
1 | CREATE TABLE IF NOT EXISTS `points_board` ( |
在历史赛季榜单中,有一个下拉选框,可以选择历史赛季信息:
其实就是获取赛季表的信息【多条信息】
因此,我们需要实现一个接口,把历史赛季全部查询出来
无
查询赛季列表—>必须是当前赛季【开始时间小于等于当前时间】
既然要使用Redis的SortedSet来实现排行榜,就需要在用户每次积分变更时,累加积分到Redis的SortedSet中。因此,我们要对之前的新增积分功能做简单改造,如图中绿色部分:
在Redis中,使用SortedSet结构,以赛季的日期为key,以用户id为member,以积分和为score. 每当用户新增积分,就累加到score中,SortedSet排名就会实时更新。这样一个实时的当前赛季榜单就出现了
一旦积分微服务获取到积分,然后将积分新增到积分明细表之后,我就可以发送积分【累加】到redis!!!!
问题一:如何做排行榜?
redis的Zset数据结构【key=赛季日期,member=用户id,score=积分和】—如果有用户新增积分,那就累加到对应score上,zset就可以实时更新
问题二:积分怎么新增还是累加?
使用Zset的incrementScore方法
在个人中心,学生可以查看指定赛季积分排行榜(只显示前100 ),还可以查看自己总积分和排名。而且排行榜分为本赛季榜单和历史赛季榜单。
我们可以在一个接口中同时实现这两类榜单的查询
首先,我们来看一下页面原型(这里我给出的是原型对应的设计稿,也就是最终前端设计的页面效果):
首先我们分析一下请求参数:
然后是返回值,无论是历史榜单还是当前榜单,结构都一样。分为两部分:
综上,接口信息如下:
分为整体:
其中查询我的积分和排名:
其中查询榜单列表:
无
获得我的积分 zscore boards:202408 userId
获得我的排名 zrevrank boards:202408 userId
问题二:如何分页获取排行榜(用户,积分,排名)?
获取我的排行榜 zrevRangeWithScore start stop[start和stop要根据pageSize和pageNo推断]
积分排行榜是分赛季的,每一个月是一个赛季。因此每到每个月的月初,就会进入一个新的赛季。所有用户的积分应该清零,重新累积。
如果直接删除Redis数据,那就丢失了一个赛季 —-==持久化==—-> Mysql
假如有数百万用户,每个赛季榜单都有数百万数据。随着时间推移,历史赛季越来越多,如果全部保存到一张表中,数据量会非常恐怖!–>==海量数据存储策略==
表分区(Partition)是一种数据存储方案,可以解决单表数据较多的问题【MySQL5.1开始支持表分区功能】
如果表数据过多 —> 文件体积非常大 —> 文件跨越多个磁盘分区 —> 数据检索时的速度就会非常慢 —>【Mysql5.1引入表分区】按照某种规则,把表数据对应的ibd文件拆分成多个文件来存储。
从物理上来看,一张表的数据被拆到多个表文件存储了【多张表】
从逻辑上来看,他们对外表现是一张表【一张表】 — CRUD不会变化,只是底层MySQL处理上会有变更,检索时可以只检索某个文件就可以
例如,我们的历史榜单数据,可以按照赛季切分:
此时,赛季榜单表的磁盘文件就被分成了两个文件,但逻辑上还是一张表。CRUD不会变化,只是底层MySQL处理上会有变更,检索时可以只检索某个文件就可以
表分区的好处:
1.可以存储更多的数据,突破单表上限。甚至可以存储到不同磁盘,突破磁盘上限
2.查询时可以根据规则只检索某一个文件,提高查询效率
3.数据统计时,可以多文件并行统计,最后汇总结果,提高统计效率【分而治之,各自统计】
4.对于一些历史数据,如果不需要时,可以直接删除分区文件,提高删除效率
表分区的方式:【对数据做水平拆分】
开发者自己对表的处理,与数据库无关
从物理上来看,一张表的数据被拆到多个表文件存储了【多张表】
从逻辑上来看,【多张表】 — CRUD会变化,需要考虑取哪张表做数据处理
在开发中我们很多情况下业务需求复杂,更看重分表的灵活性。因此,我们大多数情况下都会选择分表方案。
分表的好处:
1.拆分方式更加灵活【可以水平也可以垂直】
2.可以解决单表字段过多问题【垂直分表,分在多个表】
分表的坏处:
例如,对于赛季榜单,我们可以按照赛季拆分为多张表,每一个赛季一张新的表。如图:
这种方式就是水平分表,表结构不变,仅仅是每张表数据不同。查询赛季1,就找第一张表。查询赛季2,就找第二张表。
如果一张表的字段非常多(比如达到30个以上,这样的表我们称为宽表)。宽表由于字段太多,单行数据体积就会非常大,虽然数据不多,但可能表体积也会非常大!从而影响查询效率。
例如一个用户信息表,除了用户基本信息,还包含很多其它功能信息:
无论是分区,还是分表,我们刚才的分析都是建立在单个数据库的基础上。但是单个数据库也存在一些问题:
综上,在大型系统中,我们除了要做①分表、还需要对数据做②分库—>建立综合集群。
优点:【解决了单个数据库的三大问题】
1.解决了海量数据存储问题,突破了单机存储瓶颈
2.提高了并发能力,突破了单机性能瓶颈
3.避免了单点故障
缺点:
1.成本非常高【要多个服务器,多个数据库】
2.数据聚合统计比较麻烦【因为牵扯多个数据库,有些语句会很麻烦】
3.主从同步的一致性问题【主数据库往从数据库更新,会有不可取消的延误时间,只能通过提高主从数据库网络带宽,机器性能等操作(↓)延误时间】
4.分布式事务问题【因为涉及多个数据库多个表,使用seata分布式事务可以解决】
微服务项目中,我们会按照项目模块,每个微服务使用独立的数据库,因此每个库的表是不同的
[保证单节点的高可用性]给数据库建立主从集群,主节点向从节点同步数据,两者结构一样
东林微课堂是一个教育类项目,用户规模并不会很高,一般在十多万到百万级别。因此最终的数据规模也并不会非常庞大。综合之前的分析,结合天机学堂的项目情况,我们可以对榜单数据做分表,但是暂时不需要做分库和集群。
由于我们要解决的是数据过多问题,因此分表的方式选择水平分表。具体来说,就是按照赛季拆分,每一个赛季是一个独立的表,如图:
但是,考虑我们只需要排名,积分,用户id即可—>可以删除掉season,rank两个字段【也可以减少单表存储】
1 | CREATE TABLE IF NOT EXISTS `points_board_X` |
每个赛季刚开始的时候(月初)来创建新的赛季榜单表。每个月的月初执行一个创建表的任务,我们可以利用定时任务来实现。
【由于表的名称中包含赛季id,因此在定时任务中我们还要先查询赛季信息,获取赛季id,拼接得到表名,最后创建表】
大概流程如图:
①生成上赛季表:
通过xxl-job设定定时任务[每月初]:查询赛季表上个月对应的赛季id。通过传递(表名+赛季id)在mapper层创建历史赛季表
②redis数据进入mysql表:
根据(key,pageNo,pageSize)分页查询redis数据[id(改为input自己输入,按照rank属性设置),user_id,points],然后通过saveBatch分批插入新建的数据库内[数据库名根据mybatisplus动态插件底层通过threadlocal存储表名,本质是一个拦截器,在数据到mapper和数据库打交道的时候更改数据库名],插入结束记得remove删除
使用unlike指令删除【非阻塞式】
①生成上赛季表:
通过xxl-job设定定时任务[每月初]:查询赛季表上个月对应的赛季id。通过传递(表名+赛季id)在mapper层创建历史赛季表
②redis数据进入mysql表:
根据(key,pageNo,pageSize)分页查询redis数据[id(改为input自己输入,按照rank属性设置),user_id,points],然后通过saveBatch分批插入新建的数据库内[数据库名根据mybatisplus动态插件底层通过threadlocal存储表名,本质是一个拦截器,在数据到mapper和数据库打交道的时候更改数据库名],插入结束记得remove删除
使用unlike指令删除【非阻塞式】
流程中,我们会先计算表名,然后去执行持久化,而动态表名插件就会生效,去替换表名。
因此,一旦我们计算完表名,以某种方式传递给插件中的TableNameHandler,那么就无需重复计算表名了。都是MybatisPlus内部调用的,我们无法传递参数。—> 但是可以在一个线程中实现数据共享
刚才定义的定时持久化任务,通过while死循环,不停的查询数据,直到把所有数据都持久化为止。这样如果数据量达到数百万,交给一个任务执行器来处理会耗费非常多时间—->实例多个部署,这样就会有多个执行器并行执行(但是多个执行器执行相同代码,都从第一页开始也会重复处理)—->任务分片
举例[类似于发牌]:
最终,每个执行器处理的数据页情况:
要想知道每一个执行器执行哪些页数据,只要弄清楚两个关键参数即可:
因此,现在的关键就是获取两个数据:
这两个参数XXL-JOB作为任务调度中心,肯定是知道的,而且也提供了API帮助我们获取:
这里的分片序号其实就是执行器序号,不过是从0开始,那我们只要对序号+1,就可以作为起始页码了
使用xxl-job定时每月初进行持久化:
①根据计算上个月时间创建上赛季mysql表
②根据查询出来上赛季redis数据,数据库新表名通过mp动态表名插件(本质是一个拦截器,在与mapper数据库接触过程中通过threadlocal更改数据库名)】然后查询数据
③根据非阻塞语句del删除redis上赛季数据—但是我考虑使用分片,这样导致分片1执行完异步执行删除,但是分片2执行完数据好像又回来了【针对②查询结果分页用xxlJob分片,log查日志没解决,我就打断点发现是分片次数问题,我就redis添加一个总数,一个分片执行次数,然后将删除逻辑放在一个新的定时任务,判断总数==分片执行次数,符合的情况才删除】
面试官:你在项目中负责积分排行榜功能,说说看你们排行榜怎么设计实现的?
答:我们的排行榜功能分为两部分:一个是当前赛季排行榜,一个是历史排行榜。
因为我们的产品设计是每个月为一个赛季,月初清零积分记录,这样学员就有持续的动力去学习。这就有了赛季的概念,因此也就有了当前赛季榜单和历史榜单的区分,其实现思路也不一样。
首先说当前赛季榜单,我们采用了Redis的SortedSet来实现。member是用户id,score就是当月积分总值。每当用户产生积分行为的时候,获取积分时,就会更新score值。这样Redis就会自动形成榜单了。非常方便且高效。
然后再说历史榜单,历史榜单肯定是保存到数据库了。不过由于数据过多,所以需要对数据做水平拆分,我们目前的思路是按照赛季来拆分,也就是每一个赛季的榜单单独一张表。这样做有几个好处:
因此我们就不需要用到分库分表的插件了,直接在业务层利用MybatisPlus就可以实现动态表名,动态插入了。简单高效。
我们会利用一个定时任务在每月初生成上赛季的榜单表,然后再用一个定时任务读取Redis中的上赛季榜单数据,持久化到数据库中。最后再有一个定时任务清理Redis中的历史数据。
这里要说明一下,这里三个任务是有关联的,之所以让任务分开定义,是为了避免任务耦合。这样在部分任务失败时,可以单独重试,无需所有任务从头重试。
当然,最终我们肯定要确保这三个任务的执行顺序,一定是依次执行的[通过xxlJob分布式调度完成,弥补单体的springTask框架不能顺序执行的毛病]
面试官追问:你们使用Redis的SortedSet来保存榜单数据,如果用户量非常多怎么办?
首先Redis的SortedSet底层利用了跳表机制,性能还是非常不错的。即便有百万级别的用户量,利用SortedSet也没什么问题,性能上也能得到保证。在我们的项目用户量下,完全足够。
当系统用户量规模达到数千万,乃至数亿时,我们可以采用分治的思想,将用户数据按照积分范围划分为多个桶。
然后为每个桶创建一个SortedSet类型的key,这样就可以将数据分散,减少单个KEY的数据规模了。
而要计算排名时,只需要按照范围查询出用户积分所在的桶,再累加分值范围比他高的桶的用户数量即可。依然非常简单、高效。
面试官追问:你们使用历史榜单采用的定时任务框架是哪个?处理数百万的榜单数据时任务是如何分片的?你们是如何确保多个任务依次执行的呢?
答:我们采用的是XXL-JOB框架。
XXL-JOB自带任务分片广播机制,每一个任务执行器都能通过API得到自己的分片编号、总分片数量。在做榜单数据批处理时,我们是按照分页查询的方式:
此时,第一个分片处理的数据就是第1、4、7、10、13等几页数据,第二个分片处理的就是第2、5、8、11、14等页的数据,第三个分片处理的就是第3、6、9、12、15等页的数据。
这样就能确保所有数据都会被处理,而且每一个执行器都执行的是不同的数据了。
最后,要确保多个任务的执行顺序,可以利用XXL-JOB中的子任务功能。比如有任务A、B、C,要按照字母顺序依次执行,我们就可以将C设置为B的子任务,再将B设置为A的子任务。然后给A设置一个触发器。
这样,当A触发时,就会依次执行这三个任务了。