0%

八种方式-总纲

选取最适用的字段属性

  1. mysql 可以很好的支持大数据量的存取,但一般来说,数据库中的表越小,其查询的速度也就越快。所以,可以在建表的时候,为了获取更好的性能,将表中的字段长度设的尽可能的小。
  2. 尽可能将字段设置为 not null,这样在将来执行查询时,数据库不用去比较 null 值。
  3. 对于部分的文本字段,可以定义为枚举类型,MySQL 会把 enum 类型的数据当作数值型来处理,而数值型数据被处理起来的速度要比文本类型快的多。这样我们就可以提高数据库的性能。

使用连接来代替子查询

MySQL 从 4.1 开始支持 SQL 的子查询。这个技术可以使用 SELECT 语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。
使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死,并且写起来也很容易。
连接(JOIN)..之所以更有效率一些,是因为 MySQL 不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。
建立对外键的索引可提高连接的效率。

使用联合来代替手动创建临时表

MySQL 从 4.0 的版本开始支持 union 查询,它可以把需要使用临时表的两条或更多的 select 查询合并的一个查询中。在客户端的查询会话结束的时候,临时表会被自动删除,从而保证数据库整齐、高效。
使用 union 来创建查询的时候,我们只需要用 UNION 作为关键字把多个 select 语句连接起来就可以了,要注意的是所有 select 语句中的字段数目要想同。下面的例子就演示了一个使用 UNION 的查询。

事务

尽管我们可以使用子查询(Sub-Queries)、连接(JOIN)和联合(UNION)来创建各种各样的查询,但不是所有的数据库操作都可以只用一条或少数几条 SQL 语句就可以完成的。更多的时候是需要用到一系列的语句来完成某种工作。但是在这种情况下,当这个语句块中的某一条语句运行出错的时候,整个语句块的操作就会变得不确定起来。设想一下,要把某个数据同时插入两个相关联的表中,可能会出现这样的情况:第一个表中成功更新后,数据库突然出现意外状况,造成第二个表中的操作没有完成,这样,就会造成数据的不完整,甚至会破坏数据库中的数据。要避免这种情况,就应该使用事务,它的作用是:要么语句块中每条语句都操作成功,要么都失败。换句话说,就是可以保持数据库中数据的一致性和完整性。事物以 BEGIN 关键字开始,COMMIT 关键字结束。在这之间的一条 SQL 操作失败,那么,ROLLBACK 命令就可以把数据库恢复到 BEGIN 开始之前的状态。

1
2
3
4
BEGIN;
INSERT INTO salesinfo SET CustomerID=14;
UPDATE inventory SET Quantity=11 WHERE item='book';
COMMIT;

事务的另一个重要作用是当多个用户同时使用相同的数据源时,它可以利用锁定数据库的方法来为用户提供一种安全的访问方式,这样可以保证用户的操作不被其它的用户所干扰。

锁定表

尽管事务是维护数据库完整性的一个非常好的方法,但却因为它的独占性,有时会影响数据库的性能,尤其是在很大的应用系统中。由于在事务执行的过程中,数据库将会被锁定,因此其它的用户请求只能暂时等待直到该事务结束。如果一个数据库系统只有少数几个用户来使用,事务造成的影响不会成为一个太大的问题;但假设有成千上万的用户同时访问一个数据库系统,例如访问一个电子商务网站,就会产生比较严重的响应延迟。
其实,有些情况下我们可以通过锁定表的方法来获得更好的性能。下面的例子就用锁定表的方法来完成前面一个例子中事务的功能。

1
2
3
4
5
LOCK TABLE inventory WRITE SELECT Quantity FROM inventory WHERE Item='book';

...

UPDATE inventory SET Quantity=11 WHERE Item='book'; UNLOCKTABLES

这里,我们用一个 select 语句取出初始数据,通过一些计算,用 update 语句将新值更新到表中。包含有 WRITE 关键字的 LOCKTABLE 语句可以保证在 UNLOCKTABLES 命令被执行之前,不会有其它的访问来对 inventory 进行插入、更新或者删除的操作。

使用外键

锁定表的方法可以维护数据的完整性,但是它却不能保证数据的关联性。这个时候我们就可以使用外键。
一般而言,不推荐使用外键保持业务数据的完整性,业务数据的完整控制应该位于业务逻辑代码里,不由数据库控制。
如果要在 MySQL 中使用外键,一定要记住在创建表的时候将表的类型定义为事务安全表 InnoDB 类型。该类型不是 MySQL 表的默认类型。定义的方法是在 CREATETABLE 语句中加上 TYPE=INNODB。**

使用索引

索引是提高数据库性能的常用方法,它可以令数据库服务器以比没有索引快得多的速度检索特定的行,尤其是在查询语句当中包含有 MAX(),MIN()和 ORDERBY 这些命令的时候,性能提高更为明显。
一般说来,索引应建立在那些将用于 JOIN,WHERE 判断和 ORDERBY 排序的字段上。尽量不要对数据库中某个含有大量重复的值的字段建立索引。对于一个 ENUM 类型的字段来说,出现大量重复值是很有可能的情况。

优化的查询语句

绝大多数情况下,使用索引可以提高查询的速度,但如果 SQL 语句使用不恰当的话,索引将无法发挥它应有的作用。

  1. 首先,最好是在相同类型的字段间进行比较的操作。

    在 MySQL3.23 版之前,这甚至是一个必须的条件。例如不能将一个建有索引的 INT 字段和 BIGINT 字段进行比较;但是作为特殊的情况,在 CHAR 类型的字段和 VARCHAR 类型字段的字段大小相同的时候,可以将它们进行比较。

  2. 其次,在建有索引的字段上尽量不要使用函数进行操作。

  3. 第三,在搜索字符型字段时,我们有时会使用 LIKE 关键字和通配符,这种做法虽然简单,但却也是以牺牲系统性能为代价的。

  4. 最后,应该注意避免在查询中让 MySQL 进行自动类型转换,因为转换过程也会使索引变得不起作用。


count(*)的优化

针对没有 where 条件的 count(_),mysql 会优化,会采用成本最小的辅助索引查询计数。其性能反而最好。
mysql 会根据查询计划计算消耗资源,选择消耗资源最少的执行。
我说 SELECT COUNT(_) 会造成全表扫描,面试官让我回去等通知


explain 解释

mysql 中可以使用 explain 查看查询语句执行计划。

idselect_typetabletypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEbw_batchALL

解释

type:最重要的,是否使用到索引。类型有:效果由好到差。

system
const:通常情况下,如果将一个主键放置到 where 后面作为条件查询,mysql 优化器就能把这次查询优化转化为一个常量。至于如何转化以及何时转化,这个取决于优化器。
eq_ref:
ref:查找条件列使用了索引而且不为主键和 unique。其实,意思就是虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
fulltext:
ref_or_null:
index_merge:
unique_subquery:
index_subquery:
range:有范围的索引扫描,相对于 index 的全表扫描,他有范围限制,因此要优于 index。
index:按照索引的逻辑全表扫描。
ALL:全表扫描。

一般要达到 range 级别,最好到 ref,indel 和 all 就需要进行优化了。

possible_keys:mysql 可能用到的索引。
key:mysql 实际执行时使用到的索引。
rows:mysql 认为执行查询时必须扫描的行数。

CAS

CAS:Compare and Swap,即比较再交换。
jdk5 增加了并发包 java.util.concurrent.*,其下面的类使用 CAS 算法实现了区别于 synchronized 同步锁的一种乐观锁。JDK 5 之前 Java 语言是靠 synchronized 关键字保证同步的,这是一种独占锁,也是是悲观锁。

原理:

对 CAS 的理解,CAS 是一种无锁算法,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。

就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

CAS 采用了指令级别的,效率高于 synchronized。

问题:

  1. 循环时间长,开销很大。
  2. 只能保证一个变量的原子操作。
  3. ABA 问题。

循环时间长,开销大:

CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。

只能保证一个变量的原子操作:

当对一个变量执行操作时,我们可以使用循环 CAS 的操作来保证原子操作,但是对于多个变量操作时,CAS 无法直接保证操作的原子性。
可以通过一下两种办法解决:

  1. 使用互斥锁来保证原子性。
  2. 将多个变量封装成对象,通过 AtomicReference 来保证原子性。

ABA 问题:

CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值  B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为  B。
如果在这段期间它的值曾经被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS 操作的“ABA”问题。Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证 CAS 的正确性。因此,在使用 CAS 前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
个人理解为:第一阶段为比较,两次 CAS 操作之间,他们的第一阶段之间。

CAS 与 synchronized

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
  3. 补充: Java 并发编程这个领域中 synchronized 关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在 JavaSE 1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。
  4. synchronized 的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和 CAS 类似的性能;而线程冲突严重的情况下,性能远高于 CAS。

线程池?类型,场景,拒绝策略,执行过程

为什么使用线程池

提高程序的执行效率

如果程序中有大量短时间任务的线程任务,由于创建和销毁线程需要在底层操作系统交互,大量时间都耗费在创建和销毁线程上,因而比较浪费时间,系统效率很低。
而线程池里的每一个线程任务结束后,并不会死亡,而是会再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率。

控制线程的数据量,防止程序崩溃

如果不加限制的创建和启动线程,很容易造成程序崩溃,比如高并发 1000W 个线程,JVM 就需要有保存 1000W 个线程的空间,这样极易出现内存溢出。
线程池中线程数量是一定的,可以有效避免线程溢出。

介绍

多线程之线程池基础 - 九分石人的个人空间 - OSCHINA - 中文开源技术交流社区
http://note.youdao.com/noteshare?id=34aae084ebbe593f23d41a5ef95ccf6d⊂=F8F2A01AF87F43198AB8D005C6FFCD19

使用场景

JAVA 线程池场景化总结
线程池的使用场景以及 java 中 ThreadPoolExecutor 类的讲解γìńɡ 雄尐年ぐ的博客-CSDN 博客线程池应用场景

合理选择线程池的大小:

CPU 核数 _ CPU 核数 _(1+等待时间 / 计算时间),在 java 中,Runtime.getRuntime().availableProcessors()可以获取当前机器的 CPU 核数。

场景分类:

  1. 并发高,业务执行时间短的任务,线程池线程数可以设置为 cpu 核数+1,减少线程上下文的切换。
  2. 并发不高,业务执行时间长的任务,要区分看:
    1. 假如是业务时间长,集中在 IO 操作上,也就是 IO 密集型业务,因为 IO 操作并不占用 cpu,所以不要让所有的 cpu 闲下来,可以加大线程池中的线程数目,让 cpu 处理更多的业务。
    2. 假如是业务时间长集中在计算上,也就是计算密集型业务,这就和 1 一样,少的线程数,减少线程上下文的切换。
  3. 并发高,业务执行时间长,解决这种类型任务的关键不在于不在于线程池而在于整体架构设计。看看这些任务里面某些数据能否做缓存是第一步,增加服务器是第二步,至于线程池的设置参考 2。业务执行时间长,看能否通过中间件对任务进行拆分和解耦。

怎么设定:

根据几个参数进行设定:

tasks:每秒的任务数,假设为 500~1000
taskcost:每个任务花费的时间,假设为 0.1s
responsetime:系统允许容忍的最大响应时间,假设为 1s

做个计算:

corePoolSize:每秒需要多少个线程处理?
threadcount = tasks / (1 / taskcost ) = tasks _ taskcout =  ( 500 ~ 1000 ) _ 0.1 = 50~100 个线程。
corePoolSize 设置应该大于 50
根据 8020 原则,如果 80%的每秒任务数小于 800,那么 corePoolSize 设置为 80 即可。
queueCapacity = ( coreSizePool / taskcost ) * responsetime

计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待 1s,超过了的需要新开线程来执行。
切记不能设置为 Integer.MAX_VALUE,这样队列会很大,线程数只会保持在 corePoolSize 大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。

maxPoolSize = ( max ( tasks ) - queueCapacity ) / ( 1 / taskcost )(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数

计算可得 maxPoolSize = (1000-80)/10 = 92

rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓存机制来处理。
keepAliveTime 和 allowCoreThreadTimeout:采用默认通常能满足


进程,线程和协程

索引的优缺点

优点:

  1. 可以快速检索,减少 IO 次数,加快检索速度。
  2. 根据索引分组和排序,可以加快分组和排序。
  3. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  4. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  5. 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点:

  1. 索引本身也是表,因此也会占用存储空间,一般来说,索引表占用空间是数据表的 1.5 倍。
  2. 索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大。
  3. 建立索引会降低数据表的修改操作(删除、增加、修改)的效率,因为在修改数据表的同时还需要修改索引表。

创建索引:

mysql 版本 5.0 之后单表最多建 16 个索引,索引最大长度 256 字节。

适合创建的情况:

  1. 在经常需要搜索的列上,可以加快搜索的速度;
  2. 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
  3. 在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
  4. 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
  5. 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
  6. 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
  7. 数据量超过 300 的表应该有索引。
  8. 索引应该建立在小字段上,大文本字段,超长字段不要建立索引。
  9. 复合索引尽量使用单字段索引代替。
    1. 选择性较好的字段作为主字段。
    2. 几个字段是否经常以 AND 方式出现在 where 语句中。
    3. 如果复合索引中的字段经常单独出现在 where 语句中,应该拆分复合索引。
    4. 字段超过 3 个,就需要考虑必要性,减少字段数量。
    5. 如果字段上既有单字段索引,又有复合索引,一般删除复合索引。

不适合创建的情况:

  1. 对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
  2. 对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
  3. 对于那些定义为text, imagebit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
  4. 当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。

索引失效

索引在不正确的使用时会失效,即语句执行计划不使用索引。

  1. like 以%开头,索引无效;当 like 前缀没有%,后缀有%时,索引有效。前缀必须准确。
  2. or 语句前后没有同时使用索引。当 or 左右查询字段只有一个是索引,该索引失效,只有当 or 左右查询字段均为索引时,才会生效。
  3. 组合索引,不是使用第一列索引,索引失效。
  4. 数据类型出现隐式转化。如 varchar 不加单引号的话可能会自动转换为 int 型,使索引无效,产生全表扫描。
  5. 在索引列上使用 IS NULL 或 IS NOT NULL 操作。索引是不索引空值的,所以这样的操作不能使用索引,可以用其他的办法处理,例如:数字类型,判断大于 0,字符串类型设置一个默认值,判断是否等于默认值即可。
  6. 在索引字段上使用 not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。
  7. 对索引字段进行计算操作、字段上使用函数。
  8. 当全表扫描速度比索引速度快时,mysql 会使用全表扫描,此时索引失效。
  9. 一次查询不能用多个索引。
没有必要使用索引的情况:
  1. 唯一性差;
  2. 频繁更新的字段不用(更新索引消耗);
  3. where 中不用的字段;
  4. 索引使用<>时,效果一般;

为什么使用 B+树做索引?


一颗高度为 3 的 B+树可以存放 21902400 个数据,足以满足大部分场景的需要。如果数据量更大就应该考虑分库分表。

  1. B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对 B 树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对 IO 读写次数就降低了。
  2. B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
  3. 由于 B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是 B 树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以 B+树更加适合在区间查询的情况,所以通常 B+树用于数据库索引。
总结:

B 树在提高了 IO 性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而 B 树不支持这样的操作或者说效率太低。


索引分类

无语,我差点被面试官怼坏了,又给我问到 MySQL 索引

索引类型:

普通索引:

最普通的索引,没有任何限制,用于加速查询。

创建:
  1. CREATE TABLE mytable (name VARCHAR(32), INDEX index_mytable_name (name));
  2. CREATE INDEX index_mytable_name ON mytable(name);
  3. ALTER TABLE mytable ADD INDEX index_mytable_name (name);

唯一索引:

索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。

创建:
  1. CREATE TABLE mytable ( name VARCHAR(32), UNIQUE index_unique_mytable_name (name));
  2. CREATE UNIQUE INDEX index_mytable_name ON mytable(name);
  3. ALTER TABLE mytable ADD UNIQUE INDEX index_mytable_name (name);

主键索引:

一种特殊的唯一索引,一个表只能有一个,不允许有空值,一般在建表的时候同时创建。

创建:
  1. CREATE TABLE mytable (id int(11) NOT NULL AUTO_INCREMENT , name VARCHAR(32), PRIMARY KEY (id));
  2. ALTER TABLE test.t1 ADD CONSTRAINT t1_pk PRIMARY KEY (id);

组合索引:

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用,使用组合索引时遵循最左前缀原则。

创建:
  1. CREATE TABLE mytable (id int(11), name VARCHAR(32), INDEX index_mytable_id_name (id,name));
  2. CREATE INDEX index_mytable_id_name ON mytable(id,name);
  3. ALTER TABLE mytable ADD INDEX index_mytable_id_name (id,name);

全文索引:

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。
fulltext 索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 where 语句的参数匹配。
fulltext 索引配合 match against 操作使用,而不是一般的 where 语句加 like。
它可以在 create table,alter table ,create index 使用,不过目前只有 char、varchar,text 列上可以创建全文索引。

创建:
  1. CREATE TABLE article (id int(11) NOT NULL AUTO_INCREMENT, title char(250) NOT NULL, contents text NULL, create_at int(10) NULL DEFAULT NULL, PRIMARY KEY (id), FULLTEXT (contents));
  2. CREATE FULLTEXT INDEX index_article_contents ON article(contents);
  3. ALTER TABLE article ADD FULLTEXT INDEX index_article_contents (contents);

组织类型:

聚簇索引:

表数据按照索引的顺序来储存的。也就是说索引项的顺序与表中记录的物理顺序一致。在 B+树的索引中叶子节点可能储存了当前的 key 值,也可能储存了当前的 key 值以及整行的数据,在一张表上最多只能创建一个聚簇索引,因为真实数据的物理顺序只有一种。

非聚簇索引:

表数据存储顺序与索引顺序无关,对于非聚簇索引,叶子节点包含索引字段值及指向数据页数据行的逻辑指针。

聚簇与非聚簇的总结区别:

  1. 聚簇索引是一种稀疏索引,数据页的上一级的索引页储存的是页指针,而不是行指针,而非聚簇索引,则是密集索引,在数据页的上一级索引页它为每一个数据行存储一条索引记录。
  2. 在 InnoDB 中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引,如果没有唯一键,则隐式的生成一个键建立索引。
  3. 当查询使用聚簇索引时,在对应的叶子节点,可以获得到整行数据,因此不用再次进行回表查询。

覆盖索引:

  1. 就是 select 的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。
  2. 索引是高效找到行的一个方法,当能通过检索索引就可以读取想要的数据,那就不需要再到数据表中读取行了。如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫 做覆盖索引。
  3. 是非聚集组合索引的一种形式,它包括在查询里的 Select、Join 和 Where 子句用到的所有列(即建立索引的字段正好是覆盖查询语句[select 子句]与查询条件[Where 子句]中所涉及的字段,也即,索引包含了查询正在查找的所有数据)。

不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引的列,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL 只能使用 B-Tree 索引做覆盖索引。


数据结构类型:

Hash 索引:

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。
对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
对于 hash 冲突的采用链表方式解决冲突,类似于 hashmap。因为索引结构是十分紧凑的,所以 hash 索引的查询很快。

限制:
  1. 哈希索引只包含哈希值和行指针,而不存储字段值,索引不能用索引中的值来避免读取行。
  2. 哈希索引数据并不是安装索引值顺序存储的,所以也就无法用于排序。
  3. 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。
  4. 哈希索引只支持等值比较查询,包括=,in(),<>。也不支持任何范围查询。
  5. 访问哈希索引的数据非常快,除非有很多哈希冲突。当出现哈希冲突时,存储引擎必须遍历链表中的所有指针,逐行进行比较,直到知道所有符合条件的行。
  6. 如果哈希冲突很高的话,一些索引维护操作的代价也会很高。例如,在某个选择性很低(冲突高)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。

B+树索引:

说的是 B 树,其实是 B+树。
MySQL 和 B 树的那些事 - 平凡希 - 博客园

线上 CPU100%,怎么排查?

解决:

  1. 先排查进程,查看占用 CPU 的进程。
  2. 定位代码查看问题。

金融支付场景,怎么保证幂等?

方案 1:

消息落库,新消息到来,先查库,若存在不处理,不存在,落库并处理。

问题:

性能差


synchronized 和 lock 锁底层的实现原理?

第七章 ReentrantLock 总结_chuifuhuo6864 的博客-CSDN 博客
synrhronized 关键字简洁、清晰、语义明确,因此即使有了 Lock 接口,使用的还是非常广泛。
其应用层的语义是可以把任何一个非 null 对象作为”锁”。
在 HotSpot JVM 实现中,锁有个专门的名字:对象监视器。
synchronized 是 jvm 层面的实现,lock 是 jdk 层面的实现。

synchronized 加锁对象:

  1. 当 synchronized 作用在方法上时,锁住的便是对象实例(this)。
  2. 当作用在静态方法时锁住的便是对象对应的 Class 实例,因为 Class 数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;
  3. 当 synchronized 作用于某一个对象实例时,锁住的便是对应的代码块。
  4. 修饰实例方法:对实例的对象进行加锁
  5. 修饰类的方法:对类对象进行加锁
  6. 修饰代码块:锁括号里面的对象进行加锁

synchronized 底层实现原理:

  1. synchronized 通过 monitor 和 mutex lock 实现了互斥
  2. synchronized 是通过对象内部的一个叫做监视器(monitor)来实现的
  3. 监视器内有个组件是计数器,默认值为 0
  4. 当计数器为 0,尝试加锁,加锁成功,计数器加 1
  5. 其他线程发现计数器不为 0,加锁失败进入阻塞

synchronized 也实现了可重入的功能,
监视器锁本质是依赖于底层的操作系统的 Mutex Lock 是互斥锁来实现的。
Mutex Lock 指令的调用需要从用户态转换到核心态,
成本非常高,造成 synchronized 效率低,
因此 synchronized 称其为“重量级锁”,
JDK 对 synchronized 进行了优化,引入了膨胀的过程:

无锁->偏向锁->轻量级锁->重量级锁即 Mutex Lock

什么是 ReetrantLock?

Java 中 Lock 接口提供了 ReetrantLock 实现类来实现可重入锁功能。
ReetrantLock 基于 AQS (抽象队列同步器)来实现加锁的获取、可重入、释放。

实现可重入的功能: 可以重复地获取自己所拥有的锁

ReetrantLock 实现原理:

锁内部有两个核心参数:
state:计数器
exclusiveOwnerThread:所有者线程

  1. 先进行 cas 算法,是原子性的同时只能一个线程,当 state 计数值为 0 时,这个锁就认为是没有被任何线程所占有的,其他线程会进入等待队列。
  2. 当一个线程请求一个未被持有的锁时,计数值将会递增。
  3. 而当线程退出同步代码时,计数值会相应的递减。当计数值为 0 时,则释放该锁。

区别:

synchronizedReetrantLock
公平非公平锁参数 true/false 可以指定公平锁还是非公平锁,公平锁即进入等待队列,先进先出
队列由于等待队列,可提供中断等待锁的线程的机制
粒度粒度较粗,只有对象类代码块三种方法实现粒度较细,调用方法更多
自动释放退出时编译器自动保证释放锁手工主动释放锁,一定记得 finallly 代码块中调用 unlock()释放锁

synchronized 的锁升级过程?

synchronized 用的锁存在 Java 对象头里,Java 对象头里的 Mark Word 默认存储对象的 HashCode、分代年龄和锁标记位。在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。
32 位 JVM 的 Mark Word 可能变化存储为以下 5 种数据:

锁状态25bit4bit1bit2bit
23bit2bit是否偏向锁锁标志位
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC 标志11
偏向锁线程 IDepoch对象分代年龄101
无锁对象 hashcode对象分代年龄001

锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级。为了提高获得锁和释放锁的效率,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。

无锁状态->偏向锁->轻量级锁->重量级锁

偏向锁:

当我们创建一个对象时,该对象的部门 Markword 关键数据如下:

bit fields是否偏向锁锁标志位
hash001

该对象被创建出来的那一刻,就有了偏向锁的标志位,这说明所有对象都是可偏向的,但所有对象的状态都为 0,这说明被创建的对象的偏向锁并没有生效。

当线程执行到临界区时,此时会利用 CAS 操作,将线程 ID 插入到 Markword 中,同时修改偏向锁的标志位。
此时 Markword 的关键信息如下:

bit fields是否偏向锁锁标志位
hashepoch101

此时偏向锁状态是 1,说明偏向锁生效了,同时也可以看到,那个线程获得了该对象的锁。

偏向锁是 jdk1.6 引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

也就是说:在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:

  1. Load-and-test,判断当前线程 id 是否与 markword 中的线程 id 相同。
  2. 如果一致,说明此线程已经获得了对象的锁,继续执行下面的代码。
  3. 如果不一致,检查对象是否可偏向。
  4. 如果还未偏向,采用 CAS 操作来竞争锁。

释放锁:

偏向锁不会主动释放,采用了一种只有竞争才会释放锁的机制,不会主动释放,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点。
步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态。
  2. 撤销偏向锁,恢复到无锁状态,或者轻量级锁状态。

安全点会导致 stw(stop the word),导致性能下降,这种情况应该禁用。

jvm 开启/关闭偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁:

自旋锁的目标是降低线程切换的成本。如果竞争激烈,我们不得不采用重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁是浪费的。轻量级锁的目标是,减少无实际竞争的情况下,使用重量级锁产生的实际消耗,包括系统调用引起的内核态与用户态的切换、线程阻塞造成的线程切换等。

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时不需要申请互斥量,仅仅将 Mark Word 中的部分字节 CAS 更新指向线程栈中的 Lock Record(Lock Record:JVM 检测到当前对象是无锁状态,则会在当前线程的栈帧中创建一个名为 LOCKRECOD 表空间用于 copy Mark word 中的数据),如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后在膨胀为重量级锁。

** 缺点:**同自旋锁相似:如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。

重量级锁:

轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的 monitor 锁来实现的,而 monitor 又依赖操作系统的 MutexLock 来实现,所以重量级锁也被称为互斥锁。
当轻量级锁经过锁撤销等步骤升级为重量级锁后,它的 Markword 部分如下:

bit fields锁标志位
指向 Mutex 的指针10

重量级锁开销大:
当系统检查到锁是重量级锁后,会把等待想要获得锁的线程阻塞,被阻塞的线程不会消耗 CPU。但阻塞或唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的。

互斥锁(重量级锁)也称为阻塞同步、悲观锁。

总结:

偏向锁,轻量级锁是乐观锁,重量级锁是悲观锁。

一个对象刚开始实例化的时候,没有任何线程来访问它。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,他会偏向这个线程,此时对象持有偏向锁。
偏向第一个线程,这个线程在修改对象头成为偏向锁的时候采用 CAS 操作,并将对象头中的 ThreadID 修改为自己的 ID,之后再次访问这个对象时,只需要对比 ID,不需要在使用 CAS 进行操作。

一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象的偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍需要持有偏向锁,则偏向锁升级为轻量级锁。如果不存在使用了,则可以将对象恢复为无锁状态,然后重新偏向。

轻量级锁认为竞争存在,但竞争的程度很轻,一般两个线程对于同一个线程的操作都会错开,或者稍微等待一会,另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程持有锁,一个线程在自旋,又有第三个线程来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止 CPU 空转。

区别:

优点缺点场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法仅存在纳秒级的差距如果线程间存在锁竞争,会存在额外的锁撤销的消耗只有一个线程访问同步快的场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗 CPU追求响应时间,同步块执行非常快
重量级锁线程的竞争不使用自旋,不会消耗 CPU线程阻塞,响应时间缓慢追求吞吐量,同步快执行时间较长

synchronized 的锁方法,锁对象头,锁代码块,怎么实现?

锁方法

1
2
3
public synchronized void method() {
// todo
}

锁方法,锁住了整个方法的内容,同时只能有一个线程进入该方法。

1
2
3
4
5
public void method() {
synchronized(this) {
// todo
}
}

锁代码块,锁住了整个方法的内容,同时只能有一个线程进入该方法。

锁代码块

1
2
3
4
5
public void method() {
synchronized (this) {
// todo
}
}

锁代码块,锁住了整个方法的内容,同时只能有一个线程进入该方法,就是上一个。

锁对象

1
2
3
4
5
6
private final Object object = new Object();
public void method() {
synchronized (object) {
// todo
}
}
1
2
3
4
5
public void method(Object object) {
synchronized (object) {
System.out.println(1);
}
}

以一个明确的对象作为锁,那个线程拿到锁就可以执行,其他线程必须等待该线程访问结束。
没有明确对象时,可以创建一个特殊对象作为锁。

锁类

1
2
3
4
5
6
7
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}

锁静态方法

1
2
3
public synchronized static void method() {
// todo
}

锁静态方法,与锁类相同,会锁住这个类的所有对象,即同时只能有一个线程,进入该类,执行程序。
该类的所有对象公用一把锁。

总结

  1. 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象公用同一把锁。
  2. 每个对象只有一个锁与之关联,谁拿到这个锁谁就可以运行他所控制的那段代码。
  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

sy

java8 有哪些新特性?

特性:

  1. lambda 表达式。
  2. 函数式接口。
  3. 方法引用与构造器引用。
  4. Stream API。
  5. 接口的默认方法与构造方法。
  6. 新时期日期 API。
  7. 其他新特性。

优点:

  1. 速度更快
  2. 代码更少
  3. 强大的 Stream API
  4. 便于并行
  5. 最大化减少空指针异常(Optional)

深拷贝与浅拷贝?

在 Java 中,除了 基本数据类型之外,还存在 类的实例对象这个引用数据类型,而一般使用 “=”号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际还是指向的同一个对象。
而浅拷贝和深拷贝就是在这个基础上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。


流式处理,Stream API

1
2
3
4
5
6
List<Integer> evens = new ArrayList<>();
for (final Integer num : nums) {
if (num % 2 == 0) {
evens.add(num);
}
}
1
2
3
List<Integer> evens = nums.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());

Stream 操作将集合转换成一个流,filter()执行我们自定义的筛选处理,这里是通过 lambda 表达式筛选出所有偶数,最后我们通过 collect()对结果进行封装处理,并通过 Collectors.toList()指定其封装成为一个 List 集合返回。
java8 的流式处理极大的简化了对集合的操作,实际上不光是集合,包括数组,文件等,只要能够转化为流,我们就可以用流式处理,类似于我们写 sql 语句一样对其操作。java8 通过内部迭代实现对流的处理,一个流式处理可以分为 3 个部分:转换成流,中间操作,终端操作。

使用:

Java8 的 Stream API 使用 - 纪莫 - 博客园
Java8 新特性之三:Stream API - 无恨之都 - 博客园

性能:

  1. 对于简单操作,比如最简单的遍历,Stream 串行 API 性能明显差于显示迭代,但并行的 Stream API 能够发挥多核特性。
  2. 对于复杂操作,Stream API 串行的性能可以和手动实现的效果匹敌,在并行操作时,Stream API 性能远超手动实现。

场景:

  1. 对于简单操作推荐使用外部迭代手动实现。
  2. 对于复杂操作,推荐使用 Stream API。
  3. 在多核情况下,推荐使用 Stream API 发挥多核优势。
  4. 单核情况下不建议使用。

如果出于代码简洁性考虑,使用 Stream API 能够写出更短的代码。即使是从性能方面说,尽可能的使用 Stream API 也另外一个优势,那就是只要 Java Stream 类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。


向上转型与向下转型

父类引用可以指向子类对象
子类引用可以指向子类对象
父类引用可以指向父类对象
子类引用不可以指向父类对象

向上转型

将子类对象直接赋值给父类引用叫向上转型
向上转型会丢失子类的特性

向下转型

将父类引用指向的子类对象再次赋值给子类引用叫做向下转型
将子类引用指向父类对象时,会在编译期报异常,ClassCastException,类型转换异常

自增主键用完了怎么办?

解决:

主键用完了会插入失败。
在 mysql 中,int 的存储范围:

最小值最大值存储大小
int(有符号)-214748364821474836484bytes
int(无符号)042949672954bytes

以无符号整型为主键,可存储 43 亿数据。
一般不会用到最大值,如果达到最大值,继续插入会报异常:

//Duplicate entry ‘4294967295’ for key ‘PRIMARY’

我们可以将主键类型修改为 bigint 继续使用。
bigint 的存储范围:

最小值最大值存储大小
bigint(有符号)-922337203685477580892233720368547758088bytes
bigint(无符号)0184467440737095516158bytes

问题:

对已经设置主键为 int 的表,且自增主键已经用完,如何改表?

1. 使用 mysql 自己的原生语句。

这种操作不支持并发的 DML 操作,会导致表无法进行 UPDATA,DELETE 操作,因此这种方法不可行。

2. 借助第三方工具。

使用业内通用的第三方工具,如,pt-osc、gh-ost。

3. 改从库表结构,然后主从切换。

方法较为复杂。因为 mysql 一般使用主从架构,从库用于读,在修改从库的表结构是,不会阻塞读操作,改完之后进行主从切换即可。主从切换时有可能造成数据丢失。

4. 分库分表

单表数据量如此巨大,使用分库分表。此时使用分布式唯一主键。


锁总结 - 九分石人的个人空间 - OSCHINA - 中文开源技术交流社区
mysql 锁分为共享锁和排他锁,也叫做读锁和写锁。
读锁是共享的,可以通过 lock in share mode 实现,这时候只能读不能写。
写锁是排他的,会阻塞其他的读锁和写锁。
从颗粒度来区分,可以分为表锁和行锁两种。
表锁会锁定整张表并且阻塞所有用户对该表的所有读写操作。
行锁又可以分为乐观锁和悲观锁,悲观锁可以通过 for update 实现,乐观锁则通过版本号实现。


不推荐使用 UUID 或者雪花 ID 作为主键

为什么 MySQL 不推荐使用 uuid 或者雪花 id 作为主键?

使用自增主键的好处

自增的主键的值是顺序的,所以 Innodb 把每一条记录都存储在一条记录的后面。当达到页面的最大填充因子时候(innodb 默认的最大填充因子是页大小的 15/16,会留出 1/16 的空间留作以后的修改):

  1. 下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费。
  2. 新插入的行一定会在原有的最大数据行的下一行,mysql 定位和寻址很快,不会为计算新行的位置而产生实际的消耗。
  3. 减少了页分裂和碎片的产生。

使用 uuid 的索引内部结构

因为 uuid 相对顺序的自增 id 来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以 innodb 无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。
这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:

  1. 写入的目标页很可能已经刷新到磁盘上,并且从缓存上移除,或者还没有被加载到缓存中,innodb 在插入之前不得不先找到并从磁盘读取内容到内存中,这将导致大量的随机 IO。
  2. 因为写入是乱序的,innodb 不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,一次插入最少需要修改 3 个页以上。
  3. 由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片。

在把随机值(uuid 和雪花 id)载入到聚簇索引(innodb 默认的索引类型)以后,有时候会需要做一次 OPTIMEIZE TABLE 来重建表并优化页的填充,这将又需要一定的时间消耗。

自增主键的缺点

  1. 别人一旦爬取你的数据库,就可以根据数据库的自增 id 获取到你的业务增长信息,很容易分析出你的经营情况。
  2. 对于高并发的负载,innodb 在按主键进行插入的时候会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入都发生在这里,并发插入会导致间隙锁竞争。
  3. Auto_Increment 锁机制会造成自增锁的抢夺,有一定的性能损失。

主从同步怎么做?

mysql 主从同步的原理:

  1. master 提交完事务后,写入 binlog。
  2. slave 连接到 master,获取 binlog。
  3. master 创建 dump 线程,推送 binlog 到 slave。
  4. slave 启动一个 IO 线程,读取同步过来的 master 的 binlog,记录到 relay log 中继日志中。
  5. slave 再开启一个线程读取 relay log 事件并在 slave 执行,完成同步。
  6. slave 记录自己的 binlog。

由于 mysql 默认的赋值方式是异步的,主库把日志发送给从库后不关系从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生:

全同步复制

主库写入 binlog 后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回 ACK 确认给主库,主库收到至少一个从库的确认就认为写操作完成。

缓存过期策略?

定期删除

redis 默认是每过 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。

惰性删除

在删除 key 的时候,并不进行删除,下一次获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。

内存淘汰机制?

  1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。
  3. allkeys-random:当内存不足以容纳新写入数据时,随机移除某个 key。
  4. volatite-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key。
  5. volatite-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
  6. volatite-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

为什么使用 redis?

优势:

  1. 速度快。
  2. 支持多种数据类型。
  3. 功能丰富。
  4. 服务器简单。
  5. 客户端语言多。
  6. 支持持久化。
  7. 高可用,主从复制,分布式。

速度快

  1. c 语言实现。
  2. 纯内存操作,数据都在内存中。
  3. 单线程,避免线程切换及线程竞争带来的开销。
  4. 采用 epoll,非阻塞 IO,不再网络 IO 浪费时间。

单线程仅仅是说在网络请求这一模块上用一个请求处理客户端的请求,像持久化它就会重开一个线程/进程去进行处理。

多种数据类型

redis 共有 8 种数据类型,常用的是前 5 种。

  1. string:字符串
  2. hash:哈希
  3. list:列表
  4. set:集合
  5. zset:有序集合
  6. bitmaps:位图
  7. hyperloglog:
  8. GEO:地理信息定位

功能丰富

  1. 缓存:基础使用,存储高频次访问的数据。
  2. 排行榜:利用 zset 数据结构做。
  3. 计数器,限速器:利用 Redis 中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等。
  4. 好友关系:利用集合的交集,并集,差集实现。
  5. 简单的消息队列:自带的发布订阅系统。
  6. session 服务器:分布式系统中存储公共 session。
  7. 分布式锁:利用 sync 命令实现。

问题:

  1. 数据量太大不适合:redis 所有数据都存储在内存中,数据量会造成内存压力大。
  2. 访问频次低:redis 为了解决高频访问的问题,采用纯内存操作,低频次数据完全是浪费感情。

bean 注入过程?

Spring 源码学习【四】依赖注入过程_一颗贪婪的星-CSDN 博客_spring 依赖注入源码
IOC 容器在初始化的过程中,首先建立 BeanDefinition 的数据结构,接下来进行依赖注入,处理 bean 之间的关系。

getBean()触发依赖注入。

getBean()方法定义在 BeanFactory 接口中,抽象基类 AbstractBeanFactory 实现的 getBean()方法。

createBean

getBean()方法中多次调用 createBean()方法。createBean()方法是用来创建 Bean 实例的,并进行一些其他操作。
createBean()方法是一个模版方法,由其子类 AbstractAutowireCapableBeanFactory 实现。
到这里,我们就已经得到依赖注入后的 Bean 实例了,这个过程看起来十分简单,但实际上有两个要点需要我们格外关注:其一,创建 Bean 实例(createBeanInstance);其二,进行依赖注入(populateBean)。

createBeanInstance

createBeanInstance 通过适当的实例化策略来创建一个 Bean 实例,这里需要注意 Spring5.0 的新特性:通过设置 Bean 创建回调来覆盖工厂方法和构造函数。
在这个过程中,通过默认构造方法(无参构造)是基本的实例化策略,这个策略 Spring 的实现中提供了两种创建 Bean 的方式:其一,通过 JVM 反射创建 Bean 实例;其二,通过 cgLib 动态代理创建 Bean 实例。

populateBean

这里就是依赖注入的核心了,在这里,首先由 InstantiationAwareBeanPostProcessors 进行依赖注入前的处理,判断是否对 Bean 的 Property 进行注入,接下来先处理 autowire 的注入,这里的 autowire 指的是在配置文件中通过 autowire=”byName”或者 autowire=”byType”等属性配置的 bean,最后处理 Property 属性注入。
最终的依赖注入实际上调用了 BeanWrapper 接口的 setPropertyValues 方法,这个方法 AbstractPropertyAccessor 实现,在这个方法中,取得了所有的属性,对每一个属性进行注入。


Bean 的作用域