JVM
发表于 更新于
本文字数: 0 阅读时长 ≈ 1 分钟
分库分表分为垂直和水平两个方式,一般来说拆分的顺序是先垂直后水平。
基于现在的微服务拆分来说,都是已经做到了垂直分库了。
将订单,用户,商品,支付,库存,预算等不同业务数据存放在不同的数据库中。
将字段比较多的表,将不常用的,数据较大的字段进行拆分。
一般拆分为主表和扩展表。
比如:订单表——>基础信息,订单扩展,收货地址。
首先根据业务场景决定使用什么字段作为分表字段(sharding_key)。
比如我们现在日订单 1000 万,我们大部分的场景来源于 C 端,我们可以用 user_id 作为 sharding_key,数据查询支持到最近 3 个月的订单,超多 3 个月的做归档处理,那么 3 个月的数量就是 9 亿,可以分为 1024 张表,那么每张表的数据大概就在 100 万左右。
比如用户 id 是 100,那么我们经过 hash(100),然后对 1024 取模,就可以落到对应的表上了。
AOP,又称面向切面编程,AOP 是一种编程思想,是面向对象编程的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程将程序抽象成各个层次的切面。
在面向切面的编程思想里面,把功能分为核心业务功能和周边功能。
周边功能在 spring 的面向切面编程 AOP 思想里,即被定义为切面。
在面向切面编程 AOP 的思想里面,核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能 “编织” 在一起,这就叫 AOP
AOP 可以将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于将来的可扩展性和可维护性。
AOP 要实现的效果是,保证开发者在不修改源代码的前提下,去为系统中的业务组件添加某种通用的功能。
AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,就是代理模式的典型应用。
比较:
类别 | 机制 | 原理 | 优点 | 缺点 |
---|---|---|---|---|
静态 AOP | 静态织入 | 在编译时,切面直接以字节码的形式编译进目标子节码文件中 | 对系统无性能影响 | 灵活性不够 |
动态 AOP | JDK 动态代理 | 在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中 | 相对于静态 AOP 更加灵活 | 切入的关注点需要实现接口,对系统有一点性能影响 |
动态子节码 | CGLib | 在运行期,目标类加载前,动态生成目标类的子类,将切面逻辑加入到子类中 | 没有接口也可以织入 | 扩展类的实例,方法用 final 修饰时则无法织入 |
自定义类加载器 | 在运行期,目标类加载前,将切面逻辑加入到目标字节码里 | 可以对绝大部分类进行织入 | 代码中如果使用了其他类加载器,则这些类不会织入 | |
字节码转换 | 在运行期,所有类加载器加载字节码前进行拦截 | 可以对所有类进行织入 |
在 springboot 中可以通过 Order 指定执行的顺序,around 与 before 比 order 小,around 与 after、afterreturning、afterthrowing 比 order 大。
异常情况下,环绕通知的后通知,返回通知会被异常通知阻断,不会执行。
本质上,spring 中通过提前暴露来解决循环依赖。
java 中的循环依赖分为两种,一种是构造器的循环依赖。另一种是属性的循环依赖。
构造器的循环依赖就是在构造方法中有属性循环依赖。
这种循环依赖无法解决,因为 JVM 虚拟机在对类进行实例化的时候,需要先实例构造器的参数,由于循环引用这个参数无法实例化,只能抛出错误。
属性的循环依赖,在类的属性中具有循环依赖。
1 | protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { |
1 | public void preInstantiateSingletons() throws BeansException { |
1 | public Object getBean(String name) throws BeansException { |
1 | protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, |
所以先后顺序是,单例对象先存在于 singletonFactories 中,后存在于 earlySingletonObjects 中,最后初始化完成后放入 singletonObjects 中。
1 | protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
1 | public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { |
此方法中分为四个步骤,
1 | protected void beforeSingletonCreation(String beanName) { |
1 | protected void afterSingletonCreation(String beanName) { |
1 | protected void addSingleton(String beanName, Object singletonObject) { |
1 | protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { |
1 | protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { |
在 addSingletonFactory 方法中,将第二个参数 ObjectFactory 存入了 singletonFactories 供其他对象依赖时调用。然后下面的populateBean 方法对刚实例化的 bean 进行属性注入(该方法关联较多,本文暂时不展开追踪了,有兴趣的园友自行查看即可),如果遇到 Spring 中的对象属性,则再通过 getBean 方法获取该对象。至此,循环依赖在 Spring 中的处理过程已经追溯完毕。
属性注入主要是在 populateBean 方法中进行的。对于循环依赖,以我们上文中的 Teacher 中注入了 Student、Student 中注入了 Teacher 为例来说明,假定 Spring 的加载顺序为先加载 Teacher,再加载 Student。
getBean 方法触发 Teacher 的初始化后:
完成 Teacher 的初始化后,Student 的初始化就简单了,因为 map 中已经存了这个单例。
至此,Spring 循环依赖的总结分析结束,一句话来概括一下:Spring 通过将实例化后的对象提前暴露给 Spring 容器中的 singletonFactories,解决了循环依赖的问题。
A 依赖 B,B 依赖 A。
构造 A 时,从一级缓存中获取不到,从二级缓存中获取不到,从三级缓存获取一个不完整的,提前曝光的 A,并且放入二级缓存,A 属性注入需要构造 B,在构造 B 时,可以从二级缓存获取 A,完成 B 的属性注入,返回 A,在完成 A 的属性注入。
Spring 中的循环依赖解决详解 - 不死码农 - 博客园
事务是应用程序中一系列严密的操作,所有操作必须完成,否则在所有操作中所做的所有更改都会撤消。也就是说事务具有原子性,一个事务中的一系列操作要么全部成功,要么一个都不做。
数据库事务正确执行的 4 个基本要素。ACID:原子性(Atomicity),一致性(Correspondence),隔离性(Isolation),持久性(Durability)。
在 mysql 通过日志来保证 ACID 特性:
总共会产生 5 种问题:第一类丢失更新,第二类丢失更新,脏读,不可重复读,幻读。
事务的隔离级别就是为了解决上面的问题诞生的,隔离级别越高,并发下产生的问题就越少,但同时付出的性能消耗就越大,很多时候必须在并发性和性能之间做一个权衡。隔离级别有 4 种,但 spring 会提供 5 种:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
READ_UNCOMMITTED | 是 | 是 | 是 | 否 |
READ_COMMITED | 否 | 是 | 是 | 否 |
REPEATABLE_READ | 否 | 否 | 是 | 否 |
SERLALIZABLE | 否 | 否 | 否 | 是 |
一般使用 READ_COMMITED,解决脏读,再通过其他办法解决不可重复读与幻读。
查看事务隔离级别使用 select @@tx_isolation。
修改当前会话事务隔离级别使用 SET session TRANSACTION ISOLATION LEVEL Serializable;
修改当前全局事务隔离级别使用 SET global TRANSACTION ISOLATION LEVEL Serializable;,修改全局的对所有会话有效,当前已经存在的会话不受影响。
MySQL 存储引擎 InnoDB 与 Myisam 的六大区别 | 菜鸟教程
mysql5.1 之前的默认存储引擎是 myisam,5.1 之后是 innodb。
myisam 支持空间函数,myisam 中数据和索引是分开的。
innodb 通过 MVCC 来支持高并发。
InnoDB 支持事务,MyISAM 不支持。
对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条 SQL 语言放在 begin 和 commit 之间,组成一个事务;
innodb 支持外键,myisam 不支持外键,对一个有外键的 innodb 表转换为 myisam 会失败。
innodb 是聚簇索引,使用 b+树作为索引结构,数据文件是和主键索引绑定在一起的,(表数据文件本身就是按 B+Tree 组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。
MyISAM 是非聚集索引,也是使用 B+Tree 作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
也就是说:InnoDB 的 B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而 MyISAM 的 B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。
innodb 不保存表的具体行数,执行 count(*)时需要扫全表。
myisam 用一个字段保存了全表的行数,执行时读出次数据即可,速度很快,不可加 where 条件。
因为 InnoDB 的事务特性,在同一时刻表中的行数对于不同的事务而言是不一样的,因此 count 统计会计算对于当前事务而言可以统计到的行数,而不是将总行数储存起来方便快速查询。InnoDB 会尝试遍历一个尽可能小的索引除非优化器提示使用别的索引。如果二级索引不存在,InnoDB 还会尝试去遍历其他聚簇索引。
如果索引并没有完全处于 InnoDB 维护的缓冲区(Buffer Pool)中,count 操作会比较费时。可以建立一个记录总行数的表并让你的程序在 INSERT/DELETE 时更新对应的数据。和上面提到的问题一样,如果此时存在多个事务的话这种方案也不太好用。如果得到大致的行数值已经足够满足需求可以尝试 SHOW TABLE STATUS。
innodb 不支持全文索引(5.7 后支持),myisam 支持全文索引,在涉及全文索引的领域的查询上 myisam 效率更高。
innodb 支持表级锁、行级锁,myisam 支持表级锁。
InnoDB 的行锁是实现在索引上的,而不是锁在物理行记录上。潜台词是,如果访问没有命中索引,也无法使用行锁,将要退化为表锁。
innodb 必须有主键,没有的话会找一个或者自动生成一个 row_id,myisam 可以没有主键。
MyISAM 表格可以被压缩后进行查询操作
Redis 有 RDB 和 AOF 两种持久化方式,持久化功能有效的避免因进程退出造成的数据丢失问题,下次重启时利用以前持久化的文件即可恢复数据。
将数据以快照的形式保存下来。触发方式为手动触发和自动触发。
手动触发分别对应 save 和 bgsave 命令。
以独立日志的方式,记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用 是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
开启 AOF 功能需要设置配置:appendonly yes,默认不开启。
命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load):
执行 bgrewriteaof 命令可以重写 AOF 文件。
AOF 重写降低了文件占用空间,除此之外,另一个目的是:更小的 AOF 文件可以更快地被 Redis 加载。
手动触发:直接调用 bgrewriteaof 命令。
自动触发:根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定自动触发时机。自动触发时机=aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentage。
binlog 为二进制格式的数据,用于备份数据库的数据。
用于复制,在主从复制中,从库利用主库上的 binlog 进行重播,实现主从同步。
用于数据库的基于时间点的还原。
binlog 是属于 MySQL Server 层面的,又称为归档日志,属于逻辑日志,是以二进制的形式记录的是这个语句的原始逻辑,依靠 binlog 是没有 crash-safe 能力的。
UUID
NOW
等在复制过程可能导致数据不一致甚至出错。undo log 是回退日志,提供回退操作。是逻辑日志。
undo 用来回滚行记录到某个版本。undo log 一般是逻辑日志,根据每行记录进行记录。
保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
事务提交后如果能够将数据缓存一段时间,而不是立即更新到数据库,就能将一次次的随机 IO 打包变成一次 IO,可以提高性能。但是这样就会丧失事务的持久性。因此引入了另外一种机制来实现持久化,即 redo log。redo 解决的问题之一就是事务执行过程中的强制刷脏。
在事务提交前,只要将 Redo Log 持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是 Redo Log 已经持久化。系统可以根据 Redo Log 的内容,将所有数据恢复到最新的状态。
redo log 是重做日志,提供前滚操作。是物理日志。
redo log 通常是物理日志,记录的是数据也页物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 mysql 服务的时候,根据 redo log 进行重做,从而达到事务的持久性这一特性。
redo log 是 InnoDB 存储引擎层的日志,又称重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。在实例和介质失败(media failure)时,redo log 文件就能派上用场,如数据库掉电,InnoDB 存储引擎会使用 redo log 恢复到掉电前的时刻,以此来保证数据的完整性。
在一条更新语句进行执行的时候,InnoDB 引擎会把更新记录写到 redo log 日志中,然后更新内存,此时算是语句执行完了,然后在空闲的时候或者是按照设定的更新策略将 redo log 中的内容更新到磁盘中,这里涉及到 WAL 即 Write-Ahead-logging 技术,他的关键点是先写日志,再写磁盘。
有了 redo log 日志,那么在数据库进行异常重启的时候,可以根据 redo log 日志进行恢复,也就达到了 cash-safe。
redo log 日志的大小是固定的,即记录满了以后就从头循环写。
将 undo log 和 redo log 结合起来,提升效率。
要从两个角度来优化,一个就是尽可能减少写入硬盘(即多个事务合并成一次落盘),另一个就是尽量顺序写入(HDD 的随机写入性能远差于顺序写入)。
Undo 记录某数据被修改前的值,可以用来在事务失败时进行 rollback;
Redo 记录某数据块被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据。
比如某一时刻数据库 DOWN 机了,有两个事务,一个事务已经提交,另一个事务正在处理。数据库重启的时候就要根据日志进行前滚及回滚,把已提交事务的更改写到数据文件,未提交事务的更改恢复到事务开始前的状态。即,当数据 crash-recovery 时,通过 redo log 将所有已经在存储引擎内部提交的事务应用 redo log 恢复,所有已经 prepared 但是没有 commit 的 transactions 将会应用 undo log 做 roll back。
原有逻辑:新逻辑:
原有的每次提交事务前同步数据并同步 undolog,会造成大量的磁盘 IO,特别是同步数据是随机 IO,效率低下。
在新逻辑中,更改数据的时候同步更改 redolog,将更改的数据缓存在缓冲区中,提交前,同步 redolog 和 undolog,因为 log 同步都是顺序 IO,提升效率。即使断电等数据丢失,未提交的数据在 undolog 中,可以进行 rollback,而已经提交的数据在 redolog 中,可以根据其恢复。而对于更改的数据,间隔时间过后在进行同步,减少数据同步次数来提升效率。
单机模式的 redis 非常简单,你只需要启动一个单一的节点就可以了,安装过程不超过 5 分钟。
通过 redis-benchmark 测试简单的命令,QPS 可达到 10w 以上,不得不说非常的让人惊艳了。
单机模式的问题也非常明显。缺乏高可用的机制!
假如 redis 进程死了,进程就只能够穿透到底层的数据库中,对业务来说非常的危险。如果你把 redis 当作数据存储来用,情况会更加严重,甚至会丢失数据。
所以最基本的 redis 部署,都会增加一个或者多个 slave(现在叫 replication)。
当主 redis 发生问题的时候,能够选取一个 slave 顶上去。
非常可惜的是,这种模式和传统的 MySQL 主从一样,切换起来比较蛋疼,需要借助外部的工具,比如keepalived
等辅助进行切换,部署和维护难度直接飙。
keepalived 是一个基于 vrrp 协议来实现的高可用方案,通过 IP 漂移实现高可用。从描述上就可以看出它需要网络管理员的参与,和我们轻量级的 redis 背道而驰。
哨兵模式就是使用额外的进程来替换 keepalived 的功能,对 redis 进程的存活性进行判断。在哨兵模式下,一旦主节点宕机,从节点作为主节点的备份可以随时顶上来。
但哨兵模式一个最大的问题,就是哨兵的数量太多,至少需要 3 个节点。
对 redis 进行仲裁的时候,需要 n/2+1 个节点投票才能确认,这也是分布式系统的一般做法 (quorum)。和 Zookeeper 类似,哨兵节点做成奇数个,是非常合适的。
哨兵模式可以通过 sentinal moniter 配置同时检测多套集群,在集群数量适中的时候,还是比较好用的。
但哨兵模式有很多隐藏的坑,比如哨兵的启动,必须在 master 存活的情况下才能正常运行;另外,如果你的 redis 配置文件中使用 rename 屏蔽了一些危险命令时,哨兵也不能够启动。
客户端在连接 redis 的时候,就不能再直接连接 redis 的实例,它需要从哨兵转上一圈,以便获取一些变更信息。
集群模式可以说是这里面最优雅的方式了。你只需要部署多个对等的 redis 节点,然后使用客户端命令进行组群就可以了。
1 | ip=192.169.0.23 |
它对节点的要求也是比较多的,一般是采用 6 个节点,三主三从。当节点超过 10 个,它的协调性就不那么灵活了,所以单集群的存储和性能上限也很快能到达。
集群模式的一些缺点很隐蔽。它的服务端节点倒是非常稳定了,但有些命令会严重影响性能。比如 mget,pipeline 等。它们需要把请求分散到多个节点执行、再聚合。节点越多,性能越低。