本文主要是介绍MySQL分布式事务多数据源分库分表常见点罗列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
这个面试题是校招时候问的,整体来说面试感受是校招的面试题目更加考验基础,考验原理。社招的话技术广度会更大,会考验框架的使用程度。问题可能没有记录全,大致考的是数据库方面以及hashmap深入的问了一下还有就是线程这块。 这个视频将会主要列数据库相关的解答。有兴趣的可以看,没有兴趣就简单看一下会不会就好了。希望可以给参加春招或者社招在找工作的朋友一点帮助。最近也金三银四了,自己也会着手看面试题。然后复习的过程就记录下来。然后关于项目的话CSDN上面也在更新,关于多租户,分库分表,数据迁移,分布式事务等也在写着文章。项目和基础是相辅相成的。私以为面试中八股文非常重要。八股不好就有种基础不扎实的感觉。先是基础的linux命令等。有些面试官喜欢考两道linux看你的水平怎么样。在然后是八股文,最好是便背边练习。最后就是java技术栈了。目前我面试也就是想面中级。感觉对中级的要求就是框架得会用。得会的多。招咱进来能干活就行。至于特别深入的底层原理现阶段还用不到。再然后就是练习了。多思考多实践,提升自身实力来应对变化。
1.三年前校招面试题
1.mysql引擎对比 吗MySQL
- 事务与外键支持:
- InnoDB:支持事务处理(ACID特性),允许行级锁定,并且能够实现复杂的多表关联更新,因此适用于需要事务安全的应用场景,如金融交易、涉及多表更新的操作等。同时,InnoDB也支持外键约束,保证数据的一致性和完整性。
- MyISAM:不支持事务和外键约束,这意味着在并发环境下如果发生错误或中断,无法通过回滚恢复到事务开始前的状态。
- 锁机制:
- InnoDB:采用行级锁定,这意味着在同一时间可以对表的不同行进行并发修改,提高并发性能,减少锁冲突。
- MyISAM:使用表级锁定,当一个线程对表进行写操作时,其他所有线程对该表的读写操作都会被阻塞,这在高并发环境下可能导致性能瓶颈。
- 崩溃恢复:
- InnoDB:具有崩溃恢复能力,通过redo日志和undo日志来确保即使在系统崩溃后也能恢复未提交或已提交的事务。
- MyISAM:不支持崩溃恢复,系统崩溃后可能需要手动修复表结构。
- 索引和缓存:
- InnoDB:主键索引是聚簇索引,即数据行物理上按照主键顺序存储,此外还支持非聚簇索引,并且将数据和索引都缓存在内存缓冲池中以提升查询性能。
- MyISAM:只缓存索引文件(key buffer),而数据文件则依赖于操作系统自身的文件系统缓存,对于全表扫描类型的查询,可能不如InnoDB高效。
2.InnoDB怎样的数据结构
- 表空间(Tablespace)
- 表空间是InnoDB逻辑上组织数据的方式,它由一个或多个数据文件(
.ibd
文件)组成,用于存储数据库表的所有数据和索引。 - 每个InnoDB表至少对应一个表空间,系统表空间用于存储系统表和其他内部信息。
- 表空间是InnoDB逻辑上组织数据的方式,它由一个或多个数据文件(
- 段(Segment)
- 在表空间内部,数据被划分为多个段。每个段管理一定范围的数据或索引内容。
- 例如,一个表会有至少两个段:一个用于数据段(存放实际数据记录),另一个用于索引段(存放与该表相关的B树索引节点)。
- 区(Extent)
- 区是InnoDB分配磁盘空间的基本单位,通常包含64个连续的页(大小为1MB)。新插入的数据会以区为单位进行预先分配,以便于快速扩展存储空间并减少碎片。
- 页(Page)
- 页是InnoDB在物理层面上处理数据和索引的基本单位,也是数据库I/O操作的最小单位,默认大小通常是16KB。
- 数据库中所有的数据行以及索引条目都存储在页内。页结构包括头部信息(如页面类型、上次修改时间等)、行数据区域和可能的空闲空间。
- 行(Row)
- 行是表中的基本数据单元,在InnoDB中,每一行记录都存储在页内的行格式中。
- InnoDB支持可变长度行格式,允许行根据其实际内容动态调整占用的空间。
- 索引结构
- InnoDB使用B+树作为其索引实现的基础结构,所有非聚簇索引都会指向主键值,而主键索引(即聚簇索引)则直接指向行数据本身。
- 聚簇索引决定了数据行在物理存储上的顺序,而非聚簇索引则保存了指向行数据的指针。
通过这样的层级结构,InnoDB能够高效地管理和访问存储在磁盘上的数据,并且提供了事务支持、行级锁定等功能,以满足复杂的企业级应用需求。
3.mysql的四种隔离级别以及对应什么问题
- Read Uncommitted(读取未提交)
- 问题:在该级别下,事务可以读取到其他事务尚未提交的数据变更,即可能出现“脏读”现象。
- 定义:事务可以读取另一个事务未提交的数据更改。
- Read Committed(读取已提交)
- 问题:解决了脏读问题,但依然存在“不可重复读”和“幻读”的可能性。
- 定义:一个事务只能看到已经提交的事务所做的更改。同一个事务中两次执行相同的查询可能会得到不同的结果,因为在这两次查询之间,其他事务可能提交了影响结果的新数据或删除了原有数据。
- Repeatable Read(可重复读)
- MySQL默认隔离级别。
- 问题:解决了脏读问题,但在该级别下,即使事务没有更新任何数据,也可能会出现“幻读”现象。幻读是指在同一事务内多次执行同样的查询语句,在这个过程中,其他事务提交了新的数据插入或者删除操作,导致前后两次查询的结果集不一致,尽管查询条件相同。
- InnoDB存储引擎通过多版本并发控制(MVCC)机制避免了不可重复读的问题,但对于新增行的幻读情况仍有可能发生。
- Serializable(可串行化)
- 问题:这是最高的隔离级别,它解决了所有并发问题,包括脏读、不可重复读和幻读。
- 定义:通过强制事务按照某种顺序进行处理,实际上实现了事务间的串行执行效果。为了达到这个目的,数据库系统会在读取数据时加上范围锁或其他形式的锁,从而最大程度地减少并发冲突,但这也可能导致严重的事务阻塞和性能下降,实际生产环境中较少使用此级别。
总结来说,随着隔离级别的提高,数据一致性保护增强,但并发性能会逐渐降低,并发问题发生的可能性也会随之减小
4.hashmap底层机制负载因子
HashMap在Java中的底层实现机制与负载因子(loadFactor)密切相关。以下是关于HashMap和其负载因子的关键点:
- 数据结构:
- HashMap基于哈希表实现,它是一个数组(称为table或bucket数组)与链表(或在JDK 1.8之后,当链表长度超过阈值时会转换为红黑树的组合结构)。
- 负载因子:
- 负载因子是衡量HashMap满的程度的一个度量,它是HashMap中已存储元素数量与HashMap容量之间的比例。
- 默认负载因子大小为0.25,在实例化HashMap时可以指定不同的负载因子值。
- 负载因子计算公式:
负载因子 = 已存储元素数量 / 容量
- 扩容操作:
- 当HashMap中的元素个数超过
容量 * 负载因子
时,即达到临界值,就会触发自动扩容操作。 - 扩容后,HashMap会创建一个新的更大的数组,并将原数组中的所有元素重新哈希到新数组中,这个过程伴随着数据迁移和桶索引的更新,因此是比较耗时的操作。
- 当HashMap中的元素个数超过
- 性能影响:
- 如果负载因子设置得过大,虽然理论上允许更高的空间利用率,但在实际应用中可能导致更多的哈希冲突,进而使得查询、插入和删除等操作的时间复杂度从理想情况下的O(1)接近甚至退化为O(n)。
- 相反,如果负载因子较小,则会更早地进行扩容,降低哈希冲突的可能性,但可能会导致空间利用率较低且增加因扩容带来的系统开销。
综上所述,负载因子在HashMap中起到了一个平衡空间效率与时间效率的作用。选择合适的负载因子对于优化HashMap的性能至关重要。默认的0.25负载因子是在时间和空间效率之间做出的一种折衷考虑,适用于大多数场景。
5.线性表常用场景
- 数组应用:
- 存储一组固定大小的同类型数据,例如用于存储学生成绩、员工信息等静态数据集合。
- 在图像处理中,像素矩阵可以通过一维或二维数组来表示,方便进行各种图像操作。
- 实现哈希表的数据存储部分,作为哈希桶来存放散列到同一位置的元素。
- 链表应用:
- 需要频繁插入和删除元素时,如操作系统中的进程控制块管理、内存分配等动态场景。
- 数据量大小未知或可能会动态增长的情况,如实现先进先出(FIFO)的数据结构——队列。
- 先进后出(LIFO)的数据结构——栈也可以用链表实现,但通常使用数组更为简单高效。
- 文件系统中的目录结构,可以用双向链表实现,便于快速添加、删除文件节点。
- 通讯录:
- 使用线性表组织联系人信息,支持按照姓名、电话号码等属性进行查找、添加和删除联系人。
- 一元多项式:
- 通过系数数组或链表来表示一元多项式的各项系数,便于计算和多项式操作。
- 数据库索引:
- 数据库管理系统中的B+树索引底层就是一种特殊的高度平衡的有序链表结构。
6.栈生活中常见的应用
栈作为一种基础的数据结构,因其“后进先出”(Last In First Out, LIFO)的特性,在生活和计算机科学中有着广泛的应用。以下是一些栈在现实生活中的例子以及它们在编程场景下的应用:
- 浏览器的历史记录:
- 用户浏览网页时,新访问的页面会被添加到历史记录栈顶,当点击“后退”按钮时,栈顶最近访问的页面被弹出并显示。
- 函数调用堆栈:
- 在计算机程序执行过程中,函数调用会形成一个调用栈。每当调用一个函数,它的上下文信息(如局部变量、返回地址等)会被压入栈中;当函数执行结束并返回时,这些信息从栈中弹出。
- 撤销操作功能:
- 在许多图形用户界面软件中,如文本编辑器、图像处理软件等,都提供了撤销/重做功能。用户的每次操作(比如删除字符、移动对象等)都被存储在栈中,撤销时只需弹出最近的操作重新应用即可。
- 括号匹配问题:
- 编译器或解释器在解析程序代码时,可以使用栈来检查括号是否正确匹配。遇到左括号就将其压入栈中,遇到右括号则尝试与栈顶的左括号进行匹配,若匹配成功则将栈顶元素弹出。
- 表达式求值:
- 计算数学表达式的逆波兰表示法(Reverse Polish Notation, RPN)计算时,可以利用栈存储运算数和处理运算符优先级,避免了使用括号。
2.队列的应用
队列是一种先进先出(First In First Out, FIFO)的数据结构,它在计算机科学和许多实际应用中扮演着至关重要的角色。以下是队列的几种常见应用场景:
- 任务调度:
- 在操作系统中,任务或进程调度通常使用一个优先级队列或者简单的FIFO队列来管理等待执行的任务。当CPU空闲时,会从就绪队列中选择下一个任务进行处理。
- 打印机任务管理:
- 在多用户共享打印机的环境中,打印请求按照它们到达的顺序被添加到打印队列中,打印机则按照队列中的顺序依次完成打印任务。
- 消息队列:
- 在分布式系统和微服务架构中,消息队列(如RabbitMQ、Kafka等)用于异步通信和解耦服务。生产者将消息发送至消息队列,消费者按消息进入队列的顺序取出并处理。
- 缓冲区:
- 数据流处理时,队列作为输入输出缓冲区,能够临时存储数据直到可以被进一步处理。例如,在网络编程中,接收缓冲区就是一个典型的队列,用于暂存接收到的数据包。
8.为什么hashmap要引入红黑树
HashMap在Java 1.8版本中引入红黑树的原因是为了优化查找性能,特别是在高并发环境下当链表长度过长时。以下是详细的解释:
- 碰撞处理:
- HashMap使用散列函数将键映射到数组的索引位置上,但在最坏情况下(例如,所有元素都映射到同一个桶),如果仅使用链表来存储哈希冲突的元素,则会导致链表过长,使得查找、插入和删除操作的时间复杂度退化为O(n),极大地降低了HashMap的整体性能。
- 平衡搜索树的引入:
- 为了改善这种情况,Java 1.8版本规定:当一个桶中的元素数量大于某个阈值(默认是8)时,HashMap会自动将该链表转换为红黑树。红黑树是一种自平衡二叉查找树,它的查找、插入和删除操作的时间复杂度都是O(logn)。
- AVL树与红黑树的选择:
- 红黑树相较于AVL树更为灵活,虽然它不像AVL树那样严格保持左右子树的高度差不超过1,但红黑树通过颜色属性和旋转操作保证了大致平衡,这使得在插入和删除时所需的旋转次数可能更少,从而在频繁变动的情况下提供更好的性能。
- 实际应用效果:
- 在大量数据且哈希冲突较多的情况下,采用红黑树结构可以显著提高HashMap的操作效率,尤其是在进行检索操作时。而在低冲突或者数据量较小的情况下,依然使用链表结构以节省空间和维持较低的初始化开销。
2.包装类和基本类的区别
包装类(Wrapper Class)和基本类型(Primitive Type)在Java中具有显著的区别,以下是它们之间主要的差异:
- 存储方式与内存分配:
- 基本类型:如int、double、char等,它们直接存储数值,在栈(对于局部变量)或堆(对于对象实例中的成员变量)中存储具体值。
- 包装类:如Integer、Double、Character等,它们是引用类型,存储的是指向实际对象的引用地址,在栈中存储引用,而对象实体则存储在堆中。
- 默认值与null:
- 基本类型:都有默认值(例如int为0,boolean为false),并且不能赋值为null。
- 包装类:可以被赋值为null,表示没有引用任何对象实例。
- 自动装箱与拆箱:
- Java SE 5及以后版本引入了自动装箱和拆箱功能,使得基本类型和对应的包装类之间能够自由转换。例如,当需要将一个基本类型放入集合(如ArrayList)时,编译器会自动将其封装为包装类;反之,从集合中取出时又会自动拆箱为基本类型。
- 泛型支持:
- 包装类:可以用于泛型,因为泛型只接受对象类型作为参数。
- 基本类型:不能直接用作泛型类型参数,但在Java 1.5之后可以通过
-Xtend
之类的注解处理器工具实现类似效果。
- 方法与特性:
- 基本类型:仅提供原始数据的存储和运算。
- 包装类:除了存储对应的基本类型值之外,还提供了很多额外的方法和特性,比如进制转换、比较大小、判断是否NaN等,以及一些特定于包装类型的缓存机制(如Integer的自动缓存-128到122之间的值)。
- 异常处理:
- 在POJO(Plain Old Java Object)中使用基本类型作为属性可能会引发NullPointerException(NPE),如果数据库查询结果为null,自动拆箱过程会导致NPE。而使用包装类可以避免这个问题,因为包装类可以接受null值。
10.基本数据类型放在方法区还是栈中
在Java中,基本数据类型(Primitive Types)的存储位置取决于它们所处的上下文:
- 作为方法中的局部变量:
- 当基本数据类型作为方法内的局部变量时,它们的值会被存储在Java虚拟机(JVM)的栈内存(Stack)中。当方法调用结束后,该栈帧会弹出,局部变量也就随之销毁。
- 作为类的成员变量:
- 如果是类的实例字段(非static),那么基本数据类型的存储位置是对象所在的堆内存(Heap)。每个对象实例都会有自己的这部分内存空间来保存实例字段。
- 若是类的静态字段(static),则基本数据类型的存储位置同样是方法区(Method Area或元空间Metaspace,根据Java版本的不同有所差异),因为静态变量属于类级别的,不依赖于任何特定对象实例。
- 常量池中的字面量:
- 对于编译期可知且不可改变的字符串字面量和整型字面量(如final static int a = 10;),它们通常被存储在方法区的常量池中。
需要注意的是,具体的内存布局和管理策略可能会受到不同Java虚拟机实现的影响。以上描述基于HotSpot JVM的经典模型。
11.线程中断的几种方式
- 使用中断标志(Volatitle或Atomic)
- 创建一个共享的、volatile修饰的布尔变量作为退出标志。当需要中断线程时,主线程或其他线程修改这个标志为
true
。在被中断的线程内部的循环体或其他合适的地方不断检查这个标志,一旦发现该标志为true
,则线程可以自行决定终止运行或者清理资源后退出。
-
使用AtomicBoolean:
- 类似于volatile变量,也可以使用
java.util.concurrent.atomic.AtomicBoolean
来代替,它提供原子性的get和set操作,同样适用于作为中断信号。
- 类似于volatile变量,也可以使用
-
调用Thread对象的interrupt()方法:
- 这是Java标准库提供的线程中断机制的核心方法。通过调用某个线程的
interrupt()
方法,可以设置该线程的中断状态为“已中断”。但请注意,调用此方法并不会立即停止线程的执行,而是给线程发送一个中断请求。
-
响应中断的方法
- 被中断的线程应该适时检查中断状态,并对中断做出适当的响应。通常做法是在循环或者阻塞操作中调用以下方法:
-
Thread.currentThread().isInterrupted()
:检查当前线程是否已被中断,但不会清除中断状态。Thread.interrupted()
:检查当前线程是否已被中断,并会清除当前线程的中断状态。
例如,在一个可能会阻塞的操作(如
Thread.sleep()
、I/O操作等)前检查中断状态,如果已经被中断,则抛出InterruptedException异常,从而跳出阻塞状态:
总之,线程中断的核心思想是协作式中断,即线程本身负责检查中断请求并做出相应的反应,而不是被外界强制性地终止其执行流。
12什么场景下使用了线程中断
- 取消长时间运行的任务:
- 当一个任务可能需要较长时间执行(如网络请求、大数据处理或循环中的长时间计算),且用户或系统希望在任务未完成前就提前结束它时,可以调用中断方法。例如,在一个异步任务框架中,如果用户关闭了应用程序或改变了需求,可以通过中断正在运行的后台线程来停止它们的工作。
- 超时处理:
- 当线程执行某个操作有特定的时间限制时,超过这个时间限制就需要中断该线程以防止其无限制地等待下去。例如,执行一个带有超时限制的数据库查询或网络I/O操作,当超时发生时,可发送中断信号让线程从阻塞状态中恢复并进行后续处理。
- 服务优雅关闭:
- 在服务器或服务端应用中,为了保证资源正常释放和数据完整性,在接收到关闭指令后,所有工作线程应尽可能地完成当前任务,并拒绝新的请求。通过中断机制,主线程可以通知工作线程开始清理工作并尽快退出。
- 多线程协作:
- 在多线程协作场景中,一个线程可能需要另一个线程完成某项任务后继续执行,或者在其他条件满足时停止自己的执行。此时,可以通过中断来实现线程间的通信和控制流切换。
- 线程池管理:
- 在线程池中,当提交到线程池的任务应该被取消时,线程池可以根据中断策略中断正在执行任务的线程。这样可以更有效地利用系统资源,避免无效劳动。
- 定期任务调度:
- 定时任务执行过程中,如果到达了预定的停止时间点或周期性任务需要跳过本次执行时,也可以通过中断已启动的定时任务线程来终止它的执行。
总之,线程中断是Java等语言中用于线程间协作以及灵活控制线程生命周期的一种重要手段,特别是在需要动态调整任务执行计划或响应外部事件变化时。
13.wait和sleep的区别
2.数据库专题面试复习
2.1数据库八股
2.1.1 innodb引擎与b+树
只有InnoDB支持事务
事务与外键
innobd 支持事务与外键,具有安全性和完整性,适合大量insert 或update 操作。
myisam 不支持事务和外键,它提供高速存储和检索,适合大量的select 查询操作。
锁机制
innoDB 支持行级锁,锁定指定记录。基于索引来加锁实现。
myisam 支持表级锁,锁定整张表。
innodb 数据结构想类似字段一样将索引存起来。支持事务。支持行级锁。
聚族索引是什么
- InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。
2.2.2事务隔离级别
读未提交 读到没有提交的数据
读已提交 (Oracle 默认隔离级别) 不能解决可重复读和幻读
可重复度 (Mysql隔离级别) 解决了可重复读,但是还是无法解决幻读
可重复读 就是在同一个事务两次读到的数据可能不一样。
你发现第一次查询张三平均每天bug个数1个,但是第二次查询本月bug总数222个,这就对不上了啊。原因是在你查询的时候,你的同事修改表把第30天的bug个数改成了200个。这样就碰到了不可重复读的情况。
幻读可以被认为是不可重复读的其中一种特殊情况。“不可重复读”和“幻读”都是读的过程中数据前后不一致,只是前者侧重于删除修改,后者侧重于新增。
2.3MySQL的 mvcc
多版本并发控制 来解决
2.4 日志
mysql binlog日志 各种日志 实现主从架构 读写分离
redo-log日志属于innodb引擎特有的日志,而mysql server 也有自己的日志 binary log
binlog日志 主要使用与主从复制和数据恢复。
ddl语句,创建表 ,修改表等等。
以事件的形式来记录。
- 主库开启binglog日志。主库把binlog传递给从库,从库拿到binlog 然后恢复数据
- binlog 有三种模式 row 每一行进行复制 statement 每一条sql 的更改 会被记录到master 的binlog 中,sql语句中万一 使用到了一些可变的函数 缺点是不可靠,还有混合型。mixd
redolog
物理日志,记录数据页状态内容,binlog 里面是逻辑日志,记录更新过程、
读写分离
*读写分离不是银弹,并不是一有性能问题就上读写分离,**而是应该先优化,例如优化慢查询,调整不合理的业务逻辑,引入缓存查询等只有确定系统没有优化空间后才考虑读写分离集群
读写分离是会导致读取不到最新数据的。主从复制延迟。
1.主服务器上的数据更改(如插入、更新、删除操作)会记录在二进制日志(Binary Log)中。每个数据更改都会生成一个日志事件(Log Event),包括事务的提交、回滚等。
2.从服务器会启动一个I/O线程,连接到主服务器并请求二进制日志。I/O线程会从主服务器读取日志事件,并将它们写入从服务器的中继日志(Relay Log)。
3.从服务器还会启动一个SQL线程,从中继日志中读取日志事件并在从服务器上执行这些操作。这样,从服务器上的数据更改会与主服务器保持一致。
作者:莫念Program
链接:https://juejin.cn/post/2252212402268543220
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.5 mysql 多数据源(datebase)
(datebase模块有相应实例)
@Configuration
public class DataSourceConfig {@Bean@ConfigurationProperties(prefix="spring.datasource.master")public DataSource datasource1(){return DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties(prefix="spring.datasource.slave")public DataSource datasource2(){return DruidDataSourceBuilder.create().build();}}
@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 当前使用数据源标识*/public static ThreadLocal<String> name = new ThreadLocal<String>();@AutowiredDataSource datasource1;@AutowiredDataSource datasource2;@Overrideprotected Object determineCurrentLookupKey() {return name.get();}@Overridepublic void afterPropertiesSet(){Map<Object, Object> properties = new HashMap<Object, Object>();properties.put("w",datasource1);properties.put("r",datasource2);super.setTargetDataSources(properties);//设置默认的数据源super.setDefaultTargetDataSource(datasource1);super.afterPropertiesSet();}}
@ResponseBody
@RequestMapping("/list")
@ApiOperation(value = "获取r数据源数据")
public R list(@RequestParam Map<String, Object> params){DynamicDataSource.name.set("r");PageUtils pageUtil = sysGeneratorService.queryList(new Query(params));return R.ok().put("page", pageUtil);
}
这种方式实现多数据源,其实有更简单的方式,比如一个在service 层面的注解就可以做到这样的事情,但是其中thead local 的应用还是很重要
2.2 sql实践与优化
2.2.1 乐观锁与悲观锁
乐观锁使用版本号来控制事务的幂等性,接口的幂等。
2.2.2索引优化 与explain
explain 查看sql执行计划,就是数据库优化看的比较频繁的东西。
2.2.3 代码生成器(datebase)
(datebase模块)
就是根据条件来注入相关的dao 层 来完成不同数据库的切换。
@Value("${renren.database: mysql}")private String database;@Bean
@Primary
@Conditional(MongoNullCondition.class)
public GeneratorDao getGeneratorDao() {if ("mysql".equalsIgnoreCase(database)) {return mySQLGeneratorDao;} else if ("oracle".equalsIgnoreCase(database)) {return oracleGeneratorDao;} else if ("sqlserver".equalsIgnoreCase(database)) {return sqlServerGeneratorDao;} else if ("postgresql".equalsIgnoreCase(database)) {return postgreSQLGeneratorDao;} else {throw new RRException("不支持当前数据库:" + database);}
}
public class TableEntity {//表的名称private String tableName;//表的备注private String comments;//表的主键private ColumnEntity pk;//表的列名(不包含主键)private List<ColumnEntity> columns;//类名(第一个字母大写),如:sys_user => SysUserprivate String className;//类名(第一个字母小写),如:sys_user => sysUserprivate String classname;
}
通过如上的映射可以完成相关的模板生成的工作。
Velocity是一个基于java的模板引擎(template engine),它允许任何人仅仅简单的使用模板语言(template language)来引用由java代码定义的对象。它作为一款成熟的基于java的模板引擎,能够帮我们实现页面静态化,同时它将Java代码与网页分开,使网站在其生命周期内更加可维护,并为Java Server Pages(JSP)或PHP提供了可行的替代方案。
提供模板服务
2.3 声明式事务与编程式事务(Spring)与分布式事务
2.3.1 Seata分布式事务
ShardingSphere支持多种分布式事务方案,包括两阶段提交(2PC)、柔性事务(BASE)等。根据你的业务需求和系统架构,选择最适合的方案:
两阶段提交(2PC):适用于对数据一致性要求极高的场景,但性能开销较大。
柔性事务(BASE):通过最终一致性来实现分布式事务,适用于对实时性要求不是非常高的场景。
- 配置ShardingSphere的分布式事务
在ShardingSphere中配置分布式事务,需要在application.yml或application.properties中进行相应配置。以下是一个使用Seata进行分布式事务管理的配置示例:
sharding:datasource:names: ds0,ds1ds0:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/ds0username: rootpassword: ds1:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/ds1username: rootpassword: transaction:seata:enabled: true
- 集成Seata
如果选择Seata作为分布式事务方案,需要在项目中集成Seata客户端,并配置Seata服务端。Seata客户端的集成通常包括添加依赖、配置Seata的相关参数等步骤
<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>最新版本</version>
</dependency>
在application.yml中配置Seata的相关参数,包括服务组、服务端地址等:
seata:enabled: trueapplication-id: ${spring.application.name}tx-service-group: my_test_tx_groupservice:vgroup-mapping:my_test_tx_group: defaultenable-degrade: falsedisable-global-transaction: falseclient:rm:report-success-enable: falsetm:commit-retry-count: 5rollback-retry-count: 5registry:type: fileconfig:type: filefile:name: file.conf
- 使用分布式事务
在业务代码中,使用@Transactional注解来标注需要在分布式事务中执行的方法。Seata会自动拦截这些方法,确保它们在分布式事务中正确执行。
import org.springframework.transaction.annotation.Transactional;@Service
public class YourService {@Transactionalpublic void yourMethod() {// 你的业务逻辑}
}
原理:
2.3.1.1 Seata
Seata(Simple Extensible Autonomous Transaction Architecture)是一个开源的分布式事务解决方案,由阿里巴巴研发并维护。它提供了对分布式环境中的数据库和微服务操作进行全局事务管理的能力,确保在分布式系统中事务的一致性。
Seata的核心组件主要包括:
- Transaction Coordinator (TC):
- 事务协调者,负责整个分布式事务的生命周期管理,包括开启、提交或回滚全局事务。
- 维护全局事务的状态,并与各资源管理器(RM)交互,驱动全局事务的最终提交或回滚。
- Transaction Manager ™:
- 事务管理器,通常集成到业务应用中,用于定义全局事务的边界,即发起全局事务、提交或回滚全局事务。
- 在业务代码中使用
@GlobalTransactional
注解来标识一个方法为全局事务。
- Resource Manager (RM):
- 资源管理器,对应各个微服务中的数据源代理,负责分支事务处理。
- RM会与TC通信,注册分支事务信息,并报告分支事务的状态。
- 对于支持XA协议的数据源,可以使用XA模式;对于不支持XA协议的数据源,Seata实现了AT(Automatic Transaction Synchronization)、TCC(Try-Confirm-Cancel)等多种模式以实现分布式事务控制。
- AT模式:
- Seata AT模式下,会在本地数据库执行SQL前记录原始SQL以及相关上下文信息,在提交时根据这些信息生成补偿SQL(Undo Log),以便在全局事务需要回滚时执行。
- 在事务提交阶段,通过对比前置镜像(Before Image)和后置镜像(After Image),向各个RM发送提交或回滚请求。
- 其他模式:
- TCC模式要求开发者提供两个额外的业务逻辑接口:Try(尝试执行)、Confirm(确认提交)和Cancel(取消回滚)。
- Saga模式是一种长事务解决方案,通过一系列的子事务及其补偿动作组成一个Saga事务,每个子事务都提交成功则整体事务成功,如果某一步失败,则根据预定义的补偿逻辑回滚前面已成功的子事务。
简而言之,Seata的工作原理是利用二阶段提交(2PC)思想扩展到分布式环境,同时引入了一种灵活的方式去适应不同的数据存储和业务场景,保证了在分布式环境下事务的最终一致性。
2.3.2 本地消息表分布式事务
本地消息表是分布式事务中一种实现最终一致性的解决方案,主要用于在微服务架构下解决跨服务、跨数据库的事务问题。该方案的核心思想是在发起方(生产者)引入一个额外的消息表来记录待发送的消息,并通过与业务数据在一个事务内提交确保消息发送的原子性。具体步骤如下:
- 创建本地消息表: 在生产者的数据库中建立一张本地消息表,用于存储待发送消息和其状态(如:待发送、已发送、已确认等)。每条消息对应一条记录。
- 执行业务操作并记录消息: 在业务逻辑中,当需要进行跨服务的分布式事务时,先执行本地数据库的事务操作,同时将待发送的消息插入到本地消息表中,这两步操作在一个数据库事务中完成,保证要么都成功要么都失败。
- 异步发送消息: 当本地事务和消息插入成功后,向消息队列(如RabbitMQ、RocketMQ或Kafka等)发送这条消息。如果发送失败,可以设置重试机制以确保消息能被正确投递到消息队列。
- 消费端处理消息: 消费者(接收方)监听消息队列,接收到消息后,在自己的服务中执行相应的业务操作,比如更新另一个服务的数据。
- 确认消息消费: 消费者在处理完业务操作后,向消息队列返回一个“消费成功”的确认信号,或者直接向生产者发送确认信息。
- 更新消息状态: 生产者接收到消费成功的确认信号后,更新本地消息表中的消息状态为已消费或已完成。
- 幂等性和补偿机制: 为了应对可能的重复消费问题,消费者端的业务处理需要设计成幂等操作。此外,对于长时间未得到确认的消息,可以通过定时任务查询本地消息表,对那些状态仍为待确认的消息进行补偿处理,如重新发送消息或触发人工干预。
通过这种方式,即使在分布式环境下,也能通过一定的延迟和额外的同步机制,实现不同服务间事务的最终一致性。虽然它不是强一致性,但在许多实际场景中能够满足业务需求且具有较高的可用性和性能优势。
2.3.2 本地消息表 与seata对比
性能角度对比:
MQ实现分布式事务在性能开销和延迟方面具有一定的优势,特别是对于可以接受最终一致性的场景。它通过异步消息传递减少了同步等待,适合高吞吐量的业务场景。
Seata为了实现强一致性,可能会引入更高的性能开销和延迟,特别是在分布式事务频繁且跨服务调用多的场景下。但它简化了分布式事务的处理,对于需要强一致性保证的业务场景更为适合。
各自缺点:
本地消息表:需要自己管理本地消息表,处理消息的可靠发送和消费确认,增加了实现的复杂度。
Seata:业务系统需要依赖Seata框架,如果框架出现问题,可能会影响到业务。以及一定的学习成本
对于强一致性场景比
2.4 MySQL分库 分表
https://blog.csdn.net/qq_21561833/article/details/136444231
上述地址是自己的IM项目分库分表设计思路和方案。分库分表简单来说就是解决一下数据库访问压力的问题,数据量太大了查询就会慢,为了解决这种压力然后产生的方案。
0.案例演示
在实现IM(即时通讯)聊天系统时,随着用户数量和消息量的增加,数据库的压力会逐渐增大。为了保证系统的可扩展性和性能,通常需要对聊天消息进行分库分表。以下是一些建议:
- 分表策略
按时间分表
优点:可以根据时间轴快速查询,旧数据归档处理也较为方便。
实现:每个时间周期(如每月、每周)创建一个新表,表名包含时间标识。
按用户分表
优点:可以将用户的消息分散到不同的表中,减少单表数据量,提高查询效率。
实现:根据用户ID的hash值分配到不同的表中,例如根据用户ID对表的数量取模来决定数据存储在哪个表。 - 分库策略
按功能分库
优点:将聊天消息、用户信息、群组信息等不同功能的数据存储在不同的数据库中,降低单个数据库的压力。
实现:创建不同的数据库实例,分别用于存储聊天记录、用户信息等数据。
按地域分库
优点:对于跨地域的IM系统,按地域分库可以减少跨地域的数据访问,提高访问速度和减少延迟。
实现:根据用户地域信息将数据存储在最近的数据库中。 - 分库分表工具
MyCAT:一个开源的数据库中间件,支持数据库的透明读写分离和分库分表。
ShardingSphere:提供了数据分片、读写分离、分布式事务等功能,支持多种数据库。 - 注意事项
一致性和事务:分库分表后,跨库事务的一致性处理会变得复杂。可以考虑使用分布式事务解决方案,如Seata。
数据迁移和扩容:随着系统的发展,可能需要对数据库进行扩容。设计时应考虑数据迁移的策略和工具。
查询效率:分库分表后,跨表或跨库的联合查询可能会影响查询效率。需要优化查询逻辑,减少跨库跨表查询的场景。
ID生成:分库分表后,需要保证全局ID的唯一性。可以使用雪花算法(Snowflake)等分布式ID或者好短生成策略。
总之,分库分表是提高IM聊天系统性能和可扩展性的有效手段。在实施分库分表策略时,需要综合考虑数据的访问模式、业务需求和系统架构,以确保数据的一致性、可用性和高效性。
ShardingSphere如何处理按月分表查询
ShardingSphere是一个开源的分布式数据库解决方案,提供了数据分片、读写分离、分布式事务等功能。对于按月分表的场景,ShardingSphere可以通过配置分片策略来实现跨表查询,以支持按月分表的数据查询。以下是如何使用ShardingSphere来处理按月分表查询的基本步骤:
- 定义分片策略
首先,需要定义一个分片策略,指定如何根据查询条件来选择对应的表。对于按月分表,可以使用时间作为分片键,根据时间来选择对应的表。
sharding:tables:your_table:actualDataNodes: ds.your_table_$->{202101..202112}tableStrategy:standard:shardingColumn: create_timepreciseAlgorithmClassName: com.yourpackage.YourPreciseShardingAlgorithm
在这个配置中,actualDataNodes定义了实际的数据节点,your_table_$->{202101…202112}表示有从your_table_202101到your_table_202112共12个表。tableStrategy定义了表的分片策略,shardingColumn指定了分片键为create_time,preciseAlgorithmClassName指定了精确分片算法的实现类。
2. 实现精确分片算法
接下来,需要实现精确分片算法。这个算法需要根据分片键的值(在这个例子中是create_time)来计算数据应该存储在哪个表中。
public class YourPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {@Overridepublic String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {SimpleDateFormat formatter = new SimpleDateFormat("yyyyMM");// 根据create_time计算表名String tableName = "your_table_" + formatter.format(shardingValue.getValue());if (availableTargetNames.contains(tableName)) {return tableName;}throw new IllegalArgumentException();}
}
这个算法将create_time的值格式化为yyyyMM格式,然后根据这个值来选择对应的表。
3. 查询数据
配置和算法准备好之后,就可以直接通过SQL来查询数据了。ShardingSphere会自动根据配置的分片策略和算法来路由到正确的表。
SELECT * FROM your_table WHERE create_time BETWEEN '2021-01-01' AND '2021-03-31';
对于这个查询,ShardingSphere会根据create_time的值,自动路由到your_table_202101、your_table_202102和your_table_202103这三个表来执行查询。
注意事项
性能优化:在进行跨表查询时,尽量减少全表扫描,利用分片键进行过滤,以提高查询效率。
时间格式:确保应用程序中使用的时间格式与分片算法中使用的时间格式一致。
分片键选择:选择合适的分片键是优化查询性能的关键。对于按月分表,通常以时间字段作为分片键。
通过上述步骤,可以利用ShardingSphere实现按月分表的数据查询,同时保持良好的查询性能和数据管理的灵活性。
1.水平分表
上面例子按照年份进行分表,或者按照单位进行分表就是简单的水平分表。
2.垂直分表
3.产生的问题
1.唯一id的问题
分布式唯一id uuid不适合作为Mysql的主键。MySQL使用的是聚簇索引。会把相邻的主键放到一块。当MySQL数据是单调递增的时候,每次只需要将数据简单的追加到索引 后面就可以了。如果MySQL主键是无序的。可能将数据插入到以前数据的中间。主键索引值更占用内存空间。
一种是雪花算法。一种是号段模式。
数据库的自增。使用一个额外表的自增id作为分布式id。号段模式优化成批量的模式。leaf 美团分布式id。将这一千个id 放在本地缓存中,
订单id的话是要按照一定的规则去生成。可能会携带一些业务字段。例如用户id 。商家id。
2.分布式事务
见上一章
3.跨库 join 操作
1.选择合适的分表字段。避免大多数跨库查询。分表字段的选择要能保证大部分高频查询场景。
避免22%的跨库问题是。
2.使用es
数据冗余到es,使用es来支持复杂查询。
1.使用es来查询出关键字段。
2.在用关键字段查询出完整数据
注意:es值存储需要搜索的字段。控制es的大小。避免es 过大。
出现分表字段支持不了的跨库查询时候。使用es来支持。
3.分开查询。然后再内存中聚合
数据库来做聚合操作和内存来做聚合操作。因为数据库资源相对于服务器资源相对来说更薄鬼一些。
尽量不要让数据库做太多复杂的查询。
4.冗余字段。每次join操作为了获取少量的字段。可以考虑冗余字段。
冗余一份就行了。笑死。
高并发情况下能用简单的方案就用简单的方案。
4.大厂常见方案
基因法加冗余数据。通过将用户id冗余订单id作为后缀。
订单号。商家号。用户号。
手机号使用搜索引擎来支持。ES.ADB。使用es来作为一个二级索引。大卖家问题。将大卖家进行单独处理。
直接使用大数据 hive 和es 来处理
复杂查询一般ES支持,统计类的可以使用hive
5.切分策略
1.范围切分。按照某个字段的范围来切分。
0-一千万放到第一个库。无需进行数据迁移,扩容方便。没有将写流量均匀均匀切分。
2.中间表映射
将分表键和数据库映射到单独的一张表上面。
每次路由前先查询这张表。
3.对分表键进行hash切分(主流)
数据分布均匀。后续扩容比较负责。
商品表和订单表。
6.资源准备和数据改造
这块还没有实践过,计划把IM全部功能写完然后再实践一下数据迁移。先准备面试。虽然可能还不一定用,就是先准备。
写入:数据双写。单写新库。
读取:读老 读新。
灰度发布
2.5 Redis 和MySQL 如何保证数据一致性
这篇关于MySQL分布式事务多数据源分库分表常见点罗列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!