Redis基础知识

一、Redis基础

1.Redis为什么快

  • 纯内存KV操作:减少了一些不必要的 I/O 操作。
  • 单线程操作:省去多线程时CPU上下文切换的时间,也不用去考虑各种锁的问题。(机器内存和网络带宽才是瓶颈)
  • 高效的数据结构:底层多种数据结构支持不同的数据类型,使得数据存储时间复杂度降到最低。
  • I/O 多路复用:多个客户端的I/O请求,当某个socket(客户端连接)的请求可读或者可写的时候,它会给一个通知,达到一个线程同时处理多个IO请求的目的。

注:Redis 单线程是说一个线程处理所有网络请求,接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器。后面的依旧是多线程。

注2:I/O :网络 I/O,多路:多个 TCP 连接,复用:共用一个线程或进程。也就是多个客户端的I/O操作复用一个线程。

注3:IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好以后就通知该进程。

注4:Redis6.0 之后引入了多线程,处理命令还是单线程,网络数据的读写这类耗时操作上是用多线程。

2.Redis与Memcached

2.1 概述:

Memcached :是高性能分布式内存缓存服务器,本质一个key-value 数据库,但不支持数据持久化,服务器关闭后全丢失。只支持key-value结构。

Redis:将大部分数据放在内存中,支持的类型有:字符串、hash、list、set、zset。

2.2 区别

两者都是内存数据库,数据都在内存。不过memcache还可用于缓存其他东西,例如图片、视频等等

1.Redis数据类型更丰富;

2.Redis支持持久化,挂掉后可以通过aof文件进行恢复,而memcache不行;

3.Redis还可以用来做消息队列、数据缓存等,而memcache主要用来做SQL语句缓存、用户临时数据缓存等;

4.Redis支持数据备份,即master-slave模式的主从数据备份;

5.Redis并不是所有数据all存在内存中,当物理内存用完时,可以将一些很久没用到的value 交换到磁盘。

3.数据结构

Redis 有8?种数据结构,主要由5种最基本的数据结构(String、List、Hash、Set、zset) 加 HyperLogLogs(基数/不重复元素 统计)、Bitmap (位存储)、Geospatial (地理位置)。

注:最新的可能不长这样,最新的有QuickList,结合了双端队列和压缩链表的优点,细节可以见 这里

粗略帮助记忆:

String:C的char*不高效,所以用了sds。

List:有序队列,所以用双端队列。

Hash:Hash和压缩链表。

Set:元素不重复,用hash结构(不重复)。

Zset:有序列表,所以用跳表。

3.1 String

动态字符串SDS(Simple Dynamic String)。String只用SDS数据结构。

简单说就是:1.用一个 len 字段记录当前字符串的长度,想要获取长度只需要获取 len 字段即可,而传统C语言需要遍历字符。2.每次空间预分配double空间 3.字符串缩短时不直接回收,记录到free里面,后续操作直接用free对应的空间。

注:Redis基于C语言开发,但是没用char*类型,引入了sds。因为c里面判断数组长度得遍历到 ‘\0’,sds可以直接知道长度。

  • 空间预分配对 SDS 修改或扩充时,会额外分配未使用的空间。len长度小于 1M,double。如果修改后长度大于 1M,那么将分配1M的使用空间。

  • 惰性空间释放SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

用途:平常的存储token等

3.2 List

List用双端列表和压缩列表两种数据结构。双端列表只有List用。

列表 List 更多是被当作队列或栈来使用。底层是用的双端队列:链表中每个节点有两个指针,prev 指向前节点,next 指向后节点。

1.头结点有len长度,能知道链表长度。2.头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点,方便对两端操作。

用途:消息队列。

3.3 Hash

List、Hash、Zset都会压缩列表。

类似于双端链表,只不过双端列表需要存储前后指针等额外的数据。所以用压缩列表。

所有的操作通过指针与解码出来的偏移量进行的。并且压缩列表的内存是连续分配的,遍历的速度很快。

注:压缩列表可以理解成数组的升级版,只不过数组每个元素空间大小固定(取最大元素的长度作为每个元素空间大小,存储不同数据可能有空隙),压缩列表更紧凑。所以每个entity会存储上一元素和当前元素的大小(用于遍历的时候区分不同元素)。

  • 好处:极大的节省了内存空间
  • 缺点:不能存储太多的元素,否则遍历效率就会很低;其次,新增或者修改某个元素时,会重新分配内存,甚至可能会引起连锁更新(每个entity记录上一节点元素大小的字段是一样的,如果跨数量级了就会连锁更新)。

用途:存储对象。

3.4 Set

用hash数据结构,能够在 O(1) 时间复杂度内取出和插入关联的值。

用途:去重

3.5 Zset

用跳跃表和压缩列表两种数据结构。跳跃表只有Zset用。

跳跃表,其在链表的基础上增加了多级索引来提升查找效率。

简单的说就是多级链表,方便快速查找。查找时间复杂度 O(logN)。

用途:排行榜

4.分布式锁

利用setnx和getset命令。Getset 命令用于设置指定 key 的值,并返回 key 的旧值。

原来分布式锁实现会比较复杂:1.要考虑锁不释放,引入expire 2.setnx和expire非原子操作,引入自动检测是否过期,过期了则getset占用锁 3.需要考虑是否会删除别人的锁(超时挂起了被别的线程占用了,恢复后把锁删了)(解决:存储时增加标记,删除判断标记是否是自己的)

注:最新的setnx和expire官方提供了原子操作,所以不用考虑后续了。

5.过期数据删除策略

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。优点:对 CPU 友好 缺点:可能会造成太多过期 key 没有被删除。
  • 定期删除 : 每隔一段时间取一批过期key 执行删除操作。优点:对内存更友好。

注:Redis对应key的过期时间,维护在过期字典里(hash表),每个key对应一个过期时间,是一个long long类型的整数。

6.内存淘汰策略

Redis 提供 6 种数据淘汰策略:

  • volatile-lru(least recently used):从设置了过期时间的key中,选最近最少使用的数据淘汰
  • volatile-ttl:从设置了过期时间的key中,挑选将要过期的数据淘汰
  • volatile-random:从设置了过期时间的key中,任意选择数据淘汰
  • allkeys-lru(least recently used):在键空间中,移除最近最少使用的 key(这个是最常用的)
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction:禁止驱逐数据,报错。

简单说就是:当空间不足时,会报错,或者随机删key,或者从有过期时间的key里面随机删或者根据LRU删或者根据快要过期删

4.0 版本后增加以下两种:

  • volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

二、数据持久化方式

rdb

aof

三、缓存一致性

1.更新缓存 VS 淘汰缓存

  • 更新缓存:数据不但写入数据库,还会写入缓存

优点:缓存不会增加一次miss,命中率高

缺点:加锁及各种并发情况可能会复杂

  • 淘汰缓存:数据只会写入数据库,不会写入缓存,只会把数据淘汰掉

优点:简单

缺点:会存在缓存miss

注:淘汰缓存后也可以通过加锁来保证只有一次命中DB,所以尽量是淘汰缓存。如果更新缓存的复杂度低那么可能才会考虑。

 

  • 先删缓存,再更新数据库  —> 进阶版:延迟双删(更新完了再删一遍)
  • 先更新数据库,再删缓存 —> 进阶版:监听binlog变更的mq,做删除

 

其他的更新缓存的不推荐:

  • 比如先更新缓存,再同步更新DB;或者先更新缓存,再异步批量更新DB。
  • 或者先更新DB再更新缓存

2.3种常用的缓存读写策略

  • Cache Aside Pattern(旁路缓存模式):先更新DB,然后删除缓存
  • Read/Write Through Pattern(读写穿透):先更新缓存再同步更新DB(缓存没有直接更新DB)
  • Write Behind Pattern(异步缓存写入):只更新缓存,异步批量更新 DB。

注:上面的读都一样,缓存有读缓存,缓存没有读DB写缓存。

注2:后两种并不常用。第三种可能点赞,或者维护浏览量这种场景可能用,吞吐量比较高。

 

3.几种缓存问题

  • 缓存穿透:请求不存在的key,每次都会落在DB上。

解决方法:布隆过滤器(一个白名单,判断请求值在不在集合中);缓存空对象(避免用一个不存在的key一直请求一直打到DB)

注:布隆过滤器可能会误判,因为采用的是hash,可能会hash冲突,那么会有小概率误判(部分不在白名单的误判成在了)。但是不影响,因为布隆过滤器说没在,那么就肯定没在。

  • 缓存击穿:缓存失效的时候,所有请求打到了DB。

解决方法:加锁

  • 缓存雪崩:同一时间不同key的缓存同时失效,请求都打到了DB。

解决方法:打散过期时间。

 

DB基础知识

一、MySQL

1.什么是MySQL

MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息等。

2.MySQL架构

  • 连接器: 身份认证、权限相关。
  • 查询缓存: 查询的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
  • 分析器: 词法分析、语法分析。
  • 优化器: 按照 MySQL 认为最优的方案去执行。
  • 执行器: 操作引擎,返回结果。
  • 存储引擎 : 主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。

3.MySQL存储引擎

主要有四种:MyISAM、InnoDB、MEMORY、Archive,其他的还有BDB等

3.1 MyISAM:

数据库存储对应三个文件,一个索引文件,一个数据文件,一个表结构文件。叶子节点data域存放的是数据的地址。

MyISAM拥有较高的插入、查询速度,但不支持事务,支持表锁,不支持外键。

MYI:索引文件

MYD:数据文件

3.2 InnoDB:

数据库存储对应的两个文件,一个表结构文件,一个数据和索引文件。叶子节点存放的是数据。

支持事务安全表(ACID),支持行级锁和外键,InnoDB是默认的MySQL引擎。

3.3 MEMORY:

MEMORY存储引擎将表中的数据存储到内存中,MySQL中使用该引擎作为临时表,存放查询的中间结果。数据的处理速度很快但是安全性不高。

3.4 Archive:

如果只有INSERT和SELECT操作,可以选择Archive,Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive。

 

注: MySQL当前默认的存储引擎是 InnoDB。 5.5.5 之前,MyISAM 是默认存储引擎。

注2:show engines;    //查看MySQL引擎

二、事务

1.什么是事务

事务是逻辑上的一组操作,要么都执行,要么都不执行。

2.数据库事务四大特性

ACID

  • Atomic(原子性)     事务必须是原子的工作单元
  • Consistent(一致性)  事务完成时,必须使所有数据都保持一致状态
  • Isolation(隔离性)    并发事务所做的修改必须和其他事务所做的修改是隔离的
  • Duration(持久性) 事务完成之后,对系统的影响是永久性的

3.MySQL事务隔离级别

3.1 并发事务带来的问题

3.1.1 脏读:在一个事务处理过程里读取了另一个未提交的事务中的数据。(读取了另外事务未提交的内容)

3.1.2 不可重复读:一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。(读取了另外事务提交的内容)

3.1.3 幻读:事务T1将表中所有某列的数据由“1”改为“2”,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

注:不可重复读侧重于修改,幻读侧重于新增删除。解决不可重复读的问题只需锁住满足条件的,解决幻读需要锁表

3.2MySQL事务隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
读已提交/不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

3.3 MySQL隔离级别的实现

基于锁和 MVCC 机制共同实现。

SERIALIZABLE 隔离级别,是通过锁来实现的。除了 SERIALIZABLE 隔离级别,其他的隔离级别都是基于 MVCC 实现。

4. Mysql里的事务处理过程

  1. 记录redo和undo log文件,确保日志在磁盘上的持久化
  2. 更新数据记录
  3. 提交事务 ,redo 写入commit记录

4.1 Undo + Redo事务的简化过程

假设有A、B两个数据,值分别为1,2,开始一个事务,事务的操作内容为:把1修改为3,2修改为4,那么实际的记录如下(简化):
A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录A=3到redo log.
E.记录B=2到undo log.
F.修改B=4.
G.记录B=4到redo log.
H.将redo log写入磁盘。
I.事务提交

5.分布式事务

单独开模块说明

三、索引

1.什么是索引

索引是帮助高效获取数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。

为什么不用hash:hash无法实现范围查找,比如搜索id>5。hash冲突。

优点:加快数据检索速度;通过唯一索引可以保证数据唯一。

缺点:创建和维护耗费时间,降低更新表的速度;存储索引耗费物理空间。

1.1 时间局部性原理和空间局部性原理

  • 时间局部性原理:查找时,会缓存。比如先查找了itemID=1的,猜想你可能还会查找=1的,则会缓存
  • 空间局部性原理:扩大范围查找。要查找itemID=1的,则同时可能会读取itemID=2或3 的。

1.2 三种存储方式可视化页面

https://www.cs.usfca.edu/~galles/visualization/BST.html

https://www.cs.usfca.edu/~galles/visualization/RedBlack.html

https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html

1.3 BTree和B+TREE

BTree也就是B-树,是一种多路搜索树,对应节点都存储数据。B+树只有叶子节点存储数据。

 

1.4 MYISAM和INNODB

MYI:索引文件

MYD:数据文件

以name为索引的B+Tree类似于上图,每次找到对应节点,该节点记录地址,然后从对应地址取得数据返回。

MyISAM为非聚集索引(数据存储在一个地方,索引存储在另外一个地方)。

 

以id(主键)查找数据时,从根节点开始找到对应节点,返回数据。而且对应id>1这种查找(空间局部性原理)会比较方便。

以name为副索引查找时,会先找到主键,然后从根节点查找对应数据返回。

 

1.5 为什么要用自增ID做主键

插入:每申请一页内存后,能充分利用内存,用完后,再去申请下一块(因为自增,所以可以充分用连续的地址空间,如果是乱序的话,就不会一块用完才去申请下一块)

查找:数字大小比较 方便查找

1.6 索引类型

细节可以见:索引类型

  • 普通索引:是最基本的索引,它没有任何限制,只是为了加快查询速度。
  • 唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
  • 主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。
  • 组合索引:指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合
  • 全文索引(Full Text) :为了检索大文本数据中的关键字的信息。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。

注:主键索引是一级索引,其他都属于二级索引,即通过二级索引找到主键id,再回表查到具体的数据。

  • 聚集索引:索引结构和数据一起存放的索引。主键索引属于聚集索引。叶子(节点存放的是数据)
  • 非聚集索引索引结构和数据分开存放的索引。二级索引属于非聚集索引。(叶子节点存放的是主键+列值)。优点:更新代价小。缺点:需要回表。

覆盖索引:需要查询的字段正好是索引的字段,就不需要再回表查询了。

2.联合/组合索引的最左前缀匹配

在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配。

如:当创建(a,b,c)复合索引时,想要索引生效的话,只能使用 a和ab、ac和abc三种组合!(ac索引其实用的是a的?)

3.索引失效的情况

  • 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)。要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。
  • 对于多列索引,不是使用的第一部分,则不会使用索引(最左前缀匹配)
  • like查询以%开头(如果是xxx%则会使用到索引)
  • 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
  • 如果mysql估计使用全表扫描要比使用索引快,则不使用索引

4.建立索引原则

  • 不为 NULL 的字段 :对于数据为 NULL 的字段,数据库较难优化。
  • 区分度高的字段。
  • 被频繁更新的字段应该慎重建立索引。
  • 索引尽可能简洁,避免冗余索引
  • 尽可能的考虑建立联合索引而不是单列索引。

四、MySQL锁

1.锁类型/级别

MySQL各存储引擎使用了三种类型(级别)的锁定机制:表级锁定,行级锁定和页级锁定。

  • 表级锁:锁定粒度大,锁冲突概率高;开销小,加锁快;不会出现死锁。主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎使用
  • 行级锁:锁定粒度小,锁冲突概率低;开销大,加锁慢;会出现死锁。主要是InnoDB存储引擎使用;
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般,主要是BerkeleyDB存储引擎使用。

适用:表级锁更适合于以查询为主,有少量按索引更新的应用;而行级锁则更适合于有大量按索引更新数据,同时又有并发查询的应用。

注:InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。

2.共享锁和排它锁

不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:

  • 共享锁(S 锁) :读锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁) :写锁/独占锁,不允许多个事务同时获取。

排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。

S 锁 X 锁
S 锁 不冲突 冲突
X 锁 冲突 冲突
# 共享锁
SELECT ... LOCK IN SHARE MODE;
# 排他锁
SELECT ... FOR UPDATE;

注:由于 MVCC 的存在,一般的 SELECT 语句,InnoDB 不会加任何锁。

3.意向锁

3.1 作用

用来快速判断是否可以对某个表使用表锁(没意向锁的话需要判断有没有行锁,得一行行遍历)。

3.2 意向锁分类:

  • 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。

注:意向锁由数据引擎自己维护,用户无法手动操作。

这四种锁的共存逻辑关系:

注:这里的锁都是说明的表级别的锁兼容关系,不涉及到行(这里的X、S都是表级锁)。意向锁不会与行级的共享锁和排他锁互斥。

比如:要加意向排它锁,如果发现表已经存在共享锁(表级别)或者排它锁(表级别),那么就会加锁不成功。

又比如:一个表已经被加了意向排它锁(证明这个表中的某行数据正在被修改),那么此时加表级别共享锁S和排它锁X都会冲突,但是加其他意向锁不会冲突,因为加了意向锁,只是代表我要修改具体某行的数据,不一定会和现有的行冲突,而且就算有冲突,应该会在后续对行加锁时可以检测出来,但是并不影响意向锁的使用。上表的关系是用来表示意向锁和表级S、X锁的关系。

 

4. InnoDB 行锁分类

  • 记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁。
  • 间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
  • 临键锁(Next-key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

5. 快照读和当前读

快照读(一致性非锁定读) :利用 MVCC 机制,读取的是记录的历史版本,也就是快照。普通的select语句。

当前读 (一致性锁定读): 就是给行记录加 X 锁或 S 锁。select for update

当前读常见 SQL 语句:

# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...

五、日志

1.三大日志

binlog:事务日志

redo log:重做日志

undo log:回滚日志

2.redo log和binlog

2.1 InnoDB 的 redo log细节

简单说就是固定大小的一块地址,可以循环写,有两个指针,一个代表写入,一个代表擦除(持久化到DB了)

注:只要redolog有了,那么数据就不会丢了。

2.2 redo log和binlog区别

  • redo log 是 InnoDB 引擎特有的,而binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

2.3 redo log和binlog协助处理流程

使用两阶段提交。先写redo log为prepare,后写binlog,然后更新redo log为commit状态。

注:提交事务先写binlog,提交redo log。如果commit阶段宕机,MySQL会检测如果有binlog,那么redo log继续提交,如果没有binlog,那么就会回滚该事务。

3.binlog

3.1 binlog的格式

  • statement : 基于sql语句的模式。update table set name =””; effect row 1000; uuid、now() other function   (默认模式)(如果对于now()函数这种问题,statement模式可能导致主从对应字段的值不一致)
  • row: 基于行模式; 存在1000条数据变更;  记录修改以后每一条记录变化的值(可以保证每一条数据一致,但是记录数据太多)
  • mixed: 混合模式,由mysql自动判断处理(对于now()函数这种自动会用row模式)

3.2 主从同步的原理

1.   master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务

2.   slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志

3.   SQL线程从中继日志读取事件,并重放其中的事件而更新slave的数据,使其与master中的数据一致

3.3 主从同步延迟产生原因

  • 当master库tps比较高的时候,产生的DDL数量超过slave一个sql线程所能承受的范围,或者slave的大型query语句产生锁等待
  • 网络传输: bin文件的传输延迟
  • 磁盘的读写耗时:文件通知更新、磁盘读取延迟、磁盘写入延迟

3.4 主从同步延迟解决方案

a.在数据库和应用层增加缓存处理,优先从缓存中读取数据(应用层在master写入数据的时候,把新数据缓存,等到缓存失效的时候,可能从库已经同步了数据)

b.减少slave同步延迟,可以修改slave库sync_binlog属性;

sync_binlog=0  文件系统来调度把binlog_cache刷新到磁盘

sync_binlog=n

c.增加延时监控

4.undolog

在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的。undo log在事务开始修改之前会记录当前的值。

六、分库分表&分布式事务

1.分库分表

1.1 跨库join问题

2. X/OpenDTP事务模型

X/Open Distributed Transaction Processing Reference Model

X/Open是一个组织机构,定义出的一套分布式事务标准, 定义了规范的API接口

 

2PC(two -phase-commit), 用来保证分布式事务的完整性

J2EE 遵循了X/open DTP规范,设计并实现了java里面的分布式事务编程接口规范-JTA

XA是X/Open DTP定义的中间件与数据库之间的接口规范。 XA接口函数由数据库厂商提供

 

2.1 X/OpenDTP 角色

AP application  具体的应用 比如上图的库存中心、订单中心

RM resouces manager   资源管理器。 数据库

TM transaction manager  事务管理器,事务协调者

3. 2PC(two -phase-commit)

(CAP:CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼)

3.1 事务过程

3.1.1 阶段一:提交事务请求(投票)

1.TM向所有的AP发送事务内容,询问是否可以执行事务的提交操作,并等待各个AP的响应

2.执行事务

各个AP节点执行事务操作,将undo和redo信息记录到事务日志中,尽量把提交过程中所消耗时间的操作和准备都提前完成后确保后续

事务提交的成功率

3.各个AP向TM反馈事务询问的响应

各个AP成功执行了事务操作,那么反馈给TM yes的response;如果AP没有成功执行事务,就反馈TM no的response

3.1.2 阶段二:执行事务提交

执行提交事务

假设一个事务的提交过程总共需要30s, 其中prepare操作需要28(事务日志落地磁盘及各种io操作),而真正commit只需要2s

那么,commit阶段发生错误的概率和prepare相比, 2/28 (<10%) .只要第一个阶段成功,那么commit阶段出现失败的概率就非常小

大大增加了分布式事务的成功概率

3.1.3 中断事务提交

3.2 2pc存在的问题

  1. 数据一致性问题
  2. 同步阻塞

4. 3PC(three phase commit)

阶段一:canCommit      询问是否可以提交

阶段二:preCommit      进行预提交,类似于2pc中的预提交

阶段三:doCommit        提交

 

4.1 改进点

  1. 增加了超时机制
  2. 第二阶段,如果协调者超时没有接受到参与者的反馈,则自动认为失败,发送abort命令
  3. 第三阶段,如果参与者超时没有接受到协调者的反馈,则自动认为成功开始提交事务(基于概率)

4.2 3pc的问题

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

 

2.1实现方式

2.2 事务悬挂

 

 

MySQL实战学习总结

一、MySql架构

1.redo log和binlog

  • redo log 是 InnoDB 引擎特有的,而binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

2.redo log和binlog协助处理流程

使用两阶段提交。先写redo log为prepare,后写binlog,然后更新redo log为commit状态。

3.InnoDB 的 redo log细节

简单说就是固定大小的一块地址,可以循环写,有两个指针,一个代表写入,一个代表擦除(持久化到DB了)

注:只要redolog有了,那么数据就不会丢了。

 

4.InnoDB 的索引模型

InnoDB 使用了 B+ 树索引模型,每一个索引在 InnoDB 里面对应一棵 B+ 树
左边为主键索引(聚簇索引),主键索引叶子节点存储的是整行数据;右边为非主键索引(普通索引、二级索引),叶子节点存储内容是主键的值。
使用主键索引查询可以直接获得数据,而使用普通索引需要先搜索得到主键的值,然后再根据主键查询主键索引得到数据(这个过程叫回表)。

 

Spring

一、Spring三大特性

IOC控制反转、AOP面向切面编程、DI依赖注入。

1.IOC控制反转

将创建对象的权利交给Spring来进行处理,可以减低计算机代码之间的耦合度。

  • 作用:解耦(减低程序间的耦合性)。
  • 优点:解耦,降低程序间的依赖关系;
  • 缺点:使用反射生成对象,损耗效率。

注:最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。

2.AOP面向切面编程

将纵向重复的代码(公共行为和逻辑)横向抽取出来并封装为一个可重用的模块,这个模块就是“切面”(Aspect)。简单的说就是将程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在每个流程里面都能执行到,而不用重复开发代码。

  • 优点:减少重复代码;提高开发效率;维护方便。
  • 主要用于:权限认证、日志、事务处理、埋点等。

3.DI依赖注入

  • 创建对象实例时,为这个对象注入对应属性值。
  • Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。

 

二、Spring MVC 和Spring boot

都属于spring这个轻量级java开发框架。springMvc属于WEB开发的MVC框架,包含模型、前端视图、控制器(也就是对应逻辑)。springBoot框架相对于springMvc框架来说, 更专注于开发微服务后台接口,不开发前端视图,简化了配置流程,不需要配置xml等。

Java基础知识

一、JAVA面向对象的三大特征

封装、继承、多态

1.封装:

隐藏实现细节,提高代码的复用性,提高了安全性。

2.继承:

多个事物直接有共同的属性和行为放到父类中,特有的放在子类中,子类可以继承父类的属性和行为。通过extends关键字。

注:JAVA只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。但可以多层继承。子类访问父类的成员变量通过super关键字。

  • 优点:可以继承父类的特征
  • 缺点:如果多层父类有相同名字的实例变量或者相同的方法,调用可能会产生歧义,不知道用的哪个父类的。

3.多态:

子类的对象放在父类的引用中,如 Animal a=new Cat(),子类的对象当父类对象来用。

优点:提高了程序的扩展性和复用性

缺点:通过父类引用操作子类对象时,只能用父类中有的方法,不能操作子类特有的方法。

注:多态的前提:1、必须有关系:继承、实现  2、通常都有重写的操作

3.1 向上转型:

当父类的引用指向子类的对象时,就发生了向上转型,即把子类类型转成了父类类型。

优点:隐藏了子类类型,提高了代码的扩展性

弊端:只能使用父类的内容,无法使用子类特有的功能

3.2 向下转型:

当要使用子类特有的功能时,就需要使用向下转型

优点:可以使用子类的 特有功能

弊端:需要面对具体的子类对象时,向下转型时容易发生ClassCastException类型转换异常。所以转型前必须要做判断。

3.3 对象的强制转换:

格式: 引用 instanceof 类型 判断当前对象是否是引用类

用法: Animal a1=new Dog();

if( !Cat instanceof a1){ //判断当前对象是不是Cat类型

}

Cat d=(Cat)a1;

二、java异常

1.异常

Java主要分为Error错误Exception异常两类。Error(错误)是程序无法处理的错误,表示运行应用程序中较严重问题。Exception(异常)是程序本身可以处理的异常。Exception(异常)又包含运行时异常非运行时异常(编译异常)

  • Error:OOM等
  • 运行时异常:ArrayIndexOutOfBoundException数组越界、NullPointerException空指针等。
  • 非运行时异常:如IOException IO异常。

2.可查的异常和不可查异常

Java的异常(Throwable)分为可查的异常(checked exceptions)不可查的异常(unchecked exceptions)

 

  • 可查异常必须要去处理,否则编译不通过,如IOException和ClassNotFoundException等。

处理异常方法:要么用try-catch语句捕获它,要么用throws子句声明抛出它。

  •  不可查异常(编译器不要求强制处置的异常)包括运行时异常和错误,不用捕获对应异常,应该找出错误程序进行修改。

三、基础知识点汇总

1.java中extends和implements

extends是继承类,implements是实现接口。 类只能继承一个,接口可以实现多个。
extends继承父类的时候可以重写父类的方法,implements实现接口,必须实现接口的所有方法。

abstract class A {
    abstract m(): void;
}


class B extends A{
}

class C implements A {
    m(): void { } //必须要实现定义在A中的所有方法
}

 

2.基本类型和包装类型

2.1 区别

  • 包装类型对应变量default值是 null ,而基本类型有默认值且不是 null
  • 包装类型可用于泛型,而基本类型不可以。
  • 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,几乎所有对象实例都存在于堆中。
  • 相比于对象类型, 基本数据类型占用的空间非常小。

2.2 装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

举例:

Integer i = 10;  //装箱
int n = i;   //拆箱

3.接口和抽象类

3.1 共同点 :

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

3.2 区别 :

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

4.深拷贝、浅拷贝、引用拷贝

浅拷贝、深拷贝、引用拷贝示意图

  • 引用拷贝:两个不同的引用指向同一个对象
  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),对象内部的属性是引用类型的话,指向的是同一地址。
  • 深拷贝 :深拷贝会完全复制整个对象。

5.== 和 equals()

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

 ==:比较的是值是否相等(基本类型比较值,引用类型(对象)比较的是内存地址)

equals():只能用来判断对象,不能用于基本数据类型。不重写等同于==,重写可以自定义。

6.String、StringBuffer、StringBuilder 的区别

  • String :里面的对象是不可变的,线程安全。适用于操作少量数据
  • StringBuffer :线程安全。适用于多线程操作大量数据。
  • StringBuilder:非线程安全的。适用于单线程操作大量数据。

四、反射

赋予了我们在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法。

优点:代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

缺点:不安全(比如无视泛型参数的安全检查),性能会变差。

应用场景:业务场景使用较少,框架使用较多。像 Spring/Spring Boot、MyBatis这些框架, Spring 里面的注解,都用了反射机制。里面也用了动态代理,动态代理的实现也依赖反射。

五、集合

1.java集合框架

两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。

2. List, Set, Queue, Map 四者的区别

List: 有序、可重复。
Set: 无序、不可重复。
Queue: 排队,有序、可重复。一般用于排队功能的叫号机。
Map: 使用键值对(key-value)存储。

List

  • ArrayList: Object[] 数组
  • VectorObject[] 数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set

  • HashSet(无序,唯一): 基于 HashMap 实现,底层采用 HashMap 来保存元素
  • LinkedHashSet: 通过 LinkedHashMap 实现。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

Queue

  • PriorityQueueObject[] 数组来实现二叉堆
  • ArrayQueueObject[] 数组 + 双指针

Map

  • HashMap: 数组+链表组成,链表长度大于阈值(默认为 8)会出现红黑树转换。
  • LinkedHashMap: 继承自 HashMap,增加了一条双向链表,可以实现顺序访问。
  • Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

注:上图是LinkedHashMap的存储结构,在hashMap的基础上多了一个双向链表,可以实现顺序访问。

3.区别

3.1 ArrayList 和 Vector :

  • ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,线程不安全 ;
  • Vector 是 List 的古老实现类,底层使用Object[ ] 存储,线程安全的。

3.2 ArrayList 与 LinkedList:

  • 数据结构:一个数组一个链表数组。LinkedList 是 双向链表 (JDK1.6 之前为循环链表,JDK1.7 取消了循环)
  • 访问速度:数组可以快速随机访问,链表不行
  • 更新:链表对中间数据插入较友好好。
  • 线程安全:都不是线程安全的;
注:很少使用LinkedList

3.3 HashSet、LinkedHashSet 和 TreeSet

  • 都是 Set 接口的实现类,元素唯一,并且都不是线程安全的。
  • HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于 FIFO 的场景,TreeSet 用于元素需要自定义排序场景。

3.4 Queue 与 Deque 

  • Queue :单端队列,一端入,另一端出,一般遵循 先进先出(FIFO) 规则
  • Deque :双端队列,在队列的两端均可以插入或删除元素。

3.5HashMap 和 Hashtable

  • HashMap 非线程安全,Hashtable 是线程安全的(内部方法基本都经过synchronized 修饰)。
  • HashMap 要比 Hashtable 效率高一点。

注:基本不会用Hashtable,要线程安全就用ConcurrentHashMap

3.6ConcurrentHashMap在jdk1.7和1.8的区别

  • 数据结构:1.7是Segment 数组 + hashMap(HashEntry 数组 + 链表),1.8是 Node 数组 + 链表 / 红黑树。相当于原来是Segment和hashMap的数组是映射关系,现在合并了。
  • 锁1.7是Segment 分段锁,针对Segment(段/槽)加锁,1.8后锁的力度更细了,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • 1.7的分段锁继承自 ReentrantLock。1.8 放弃了分段锁设计,采用 Node + CAS + synchronized 保证线程安全。
  • 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大

3.7 HashMap实现

默认大小16,负载因子0.75,超过对应阈值会扩容。

put过程:先计算key的hash值,然后低16位和高16位做异或(充分利用每一位),然后与数组长度-1做与操作确定数组的位置,然后在链表最后插入对应的节点(如果已经有相同的key会替换)。如果超过8则会有红黑树的转换。

六、并发编程

1.进程和线程

进程:是系统运行程序的基本单位。一个进程在其执行的过程中可以产生多个线程。

线程:是一个比进程更小的执行单位。同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。多线程之间切换负担要比进程小得多。

2.并发与并行的区别

:两个及两个以上的作业在同一 时间段 内执行。
:两个及两个以上的作业在同一 时刻 执行。

3.同步和异步的区别

同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。

4.线程的生命周期和状态

5.sleep() 方法和 wait() 方法

  • 都可以暂停线程的执行。
  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法(本质是每个对象都有对象锁,要释放当前线程占有的对象锁,所以操作的是对象而不是线程)。
  • wait() 需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法唤醒。sleep()方法执行完成后,线程会自动苏醒。

6.可以直接调用 Thread 类的 run 方法吗

可以但不会以多线程的方式执行,用当前线程执行的。

new 一个 Thread后,需要调用 start()方法,start()方法作用是启动一个线程处于就绪状态,当分配到时间片后就可以开始运行,这时候该线程会自动执行 run() 方法进行执行,这样就是多线程了。省略了start,那么就没有额外的线程,会用当前线程执行。

7.volatile

作用:保证变量的可见性,禁止指令重排。

7.1 如何保证变量可见性

volatile修饰的,不会有在工作内存有变量副本,线程直接从主内存取。

7.2 如何禁止指令重排

单例模式(线程安全) :

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

 uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

JVM 的指令重排可能导致执行顺序不是1->2->3,而是 1->3->2。这样再多线程情况下,1->3的时候,另一个线程读到的就不是null了。

8.synchronized

8.1 作用域

修饰方法

修饰对象:根据修饰的对象不同可以分为全局锁(xxx.class)或者代码块.

8.2 底层原理

通过 JDK 自带的 javap 命令,可以查看java字节码.class信息,发现synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令;synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。不过两者的本质都是对对象监视器 monitor 的获取。

8.3 锁升级

偏向锁、轻量级锁、重量级锁

用synchronized修饰的会有锁升级过程,如果只有一个线程获取锁,那么这个锁会偏向这个线程,叫做偏向锁;如果这时候有其他线程也来获取这个锁,那么这个锁会变成轻量级锁,获取不到锁的线程会自旋,然后重新获取;如果还是获取不到,则会变为重量级锁,获取不到的线程会阻塞(锁池状态)。

8.4 synchronized 和 ReentrantLock 的区别

  • 两者都是可重入锁
  • synchronized非公平锁,ReentrantLock默认也是非公平锁,不过可以有参数可以配置变成公平锁
  • synchronized 依赖于 JVM , ReentrantLock 依赖于 API(JDK 层面实现),需要lock() 和 unlock() 方法配合 try/finally 语句块来完成
  • ReentrantLock功能更多,如可以变成公平锁

9.ThreadLocal

为了实现每个线程都有自己的专属本地变量。通过创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,从而避免了线程安全问题。

9.1 ThreadLocal 原理

// 源码:
public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

ThreadLocal内部有一个ThreadLocalMap,每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

10.线程池

10.1为什么使用线程池

  • 降低资源消耗:可以重复利用已创建的线程,降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,用已有线程可以直接执行。
  • 方便管理:统一创建,分配,调优和监控。

10.2 实现 Runnable 接口和 Callable 接口的区别

  • Runnable自 Java 1.0 以来一直存在,Callable在 Java 1.5 中引入
  • Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以如果任务不需要返回结果或抛出异常就使用 Runnable 接口 ,看起来更简洁。

10.3  execute()方法和 submit()方法的区别

  • execute()方法:没有返回值,无法判断任务是否被线程池执行成功;
  • submit()方法:线程池会返回一个 Future 类型的对象,可以判断是否执行成功。此外可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。

10.4 四类线程池

CachedThreadPool: 可根据实际情况动态调整线程数量的线程池。

FixedThreadPool : 固定线程数量的线程池。

SingleThreadExecutor: 只有一个线程的线程池。

ScheduledThreadPool:调度类的线程池。

 

10.4.1 几个核心参数:

  • corePoolSize : 核心线程数。
  • maximumPoolSize : 最大线程数。
  • workQueue: 工作队列。
  • keepAliveTime:非核心线程(临时线程)存活时间;
  • handler :饱和策略。主要有报错、丢弃当前任务、丢弃最早未处理的任务、用提交任务的线程执行这四种。

10.4.2 大致流程

进来任务时,用核心线程做任务;如果任务超过核心线程数,则放入队列;如果任务还在增加,则创建临时线程执行,直到最大线程数;如果还在增加,则按照对应的饱和策略处理。

11.JUC 包中的4大Atomic原子类

基本类型:使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型:使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型:

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。
  • AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

12.AQS

AQS 的全称为(AbstractQueuedSynchronizer),是一个用来构建锁和同步器的框架。如: ReentrantLock,Semaphore信号量等都是用的AQS框架。

12.1 AQS 原理

通过ReentrantLock加锁说明:简单的说就是,AQS对象内部有一个volatile的int变量,初始state的值是0,一个线程要来加锁时,通过CAS把0改为1,代表加锁成功,释放锁就是再减回去,减到0就代表锁释放了。

另外AQS内部还维护着一个先进先出的等待队列,如果其他线程也来加锁,但是因为锁已经被其他线程占用而加锁失败,就会存放这些加锁失败的线程。

 

注:CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

12.2 AQS 组件总结

  • ReentrantLock:一次只允许一个线程访问某个资源,可重入。
  • Semaphore(信号量)-允许多个线程同时访问问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch (倒计时器): 可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): 让一组线程到达栅栏时被阻塞,直到最后一个线程到达时,栅栏被推倒,所有线程开始执行。

七、反射

1.概念

可以在运行时分析类以及执行类中方法,通过反射可以获取和调用对应类的属性和方法。

 像Spring/Spring Boot、MyBatis 等框架中都大量使用了反射机制,动态代理也是基于反射。

2.优缺点

  • 优点 : 更加灵活、为各种框架实现对应功能提供了便利。
  • 缺点 :无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。性能也会稍差些。

3.获取 Class 对象的四种方式

3.1. 知道具体类的情况下可以使用:

Class alunbarClass = TargetObject.class;

3.2. 通过 Class.forName()传入类的全路径获取:

Class alunbarClass1 = Class.forName("cn.test.TargetObject");

3.3. 通过对象实例instance.getClass()获取:

TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();

3.4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:

ClassLoader.getSystemClassLoader().loadClass("cn.test.TargetObject");

八、语法糖

1.概念:

计算机语言中的某种特殊语法,方便程序员使用。可以让程序更加简洁,有更高的可读性。

注:反编译,可以用java自带的javap,对.class文件进行反编译,可读性稍差。有一些网站可以把字节码反编译成对应的代码。所以查看语法糖依赖反编译。

2.常见语法糖

  • switch 支持 String 与枚举:java7开始支持switch string,正常switch对比的是基本数据类型的值或者ascii码。switch string,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查。
  • 泛型:编译阶段通过类型擦除的方式进行解语法糖。
  • 自动装箱与拆箱:装箱通过调用包装器的 valueOf 方法实现,而拆箱通过调用包装器的 xxxValue 方法实现的。
  • 可变长参数:把可变长参数变成了一个数组,方法调用传的是数组。

注:在编译阶段,如果有一些代码根本不会走到(废代码),那么编译器会不进行编译。

 

九、jvm内存模型

十、垃圾回收

1.垃圾回收算法

1.1 标记-清除算法

该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

  • 效率问题
  • 空间问题(标记清除后会产生大量不连续的碎片)

1.2 标记-复制算法

将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

注:新生代采用标记-复制算法

1.3 标记-整理算法

先标记,然后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

注:老年代有使用标记-整理算法

1.4 分代收集算法

根据对象存活周期的不同将内存分为几块。分为新生代和老年代。新生代朝生夕死的多,所以用标记-复制算法。老年代存活几率比较高,没有额外的空间来对它进行担保,所以用标记-清除或者标记-整理算法。

2.分代回收细节

分为新生代、老年代和永久代(1.8后改成meta space),新生代里面有一个Eden区,两个Survivor区,每次回收时,复制Eden区和Survivor区存活对象到另外一个Survivor区,如此反复。如果超过15次还存活的对象,则进入老年代。

注:大的对象直接进入老年代,如大的数组、字符串等。

注2:对象一般在Eden区分配,当 Eden 区没有足够空间进行分配时,将会进行 Minor GC

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):对老年代进行垃圾收集。
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  • 整堆收集 (Full GC):收集整个 Java 堆和方法区。

3.死亡对象判断

3.1 引用计数法

对象有一个引用计数器,每次被引用,计数器加 1,引用失效,计数器就减 1;为 0 的对象可以被回收。

优点:效率高

缺点:无法解决循环引用的问题

3.2 可达性分析算法

通过根节点 “GC Roots”向下搜索,当一个对象到 GC Roots 没有任何引用链相连的话,就可以被回收。

可以作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

4. 引用类型

4.1.强引用(StrongReference):

我们平常使用的Object ob = new Object(),内存不足抛出 OOM,只有ob=null,才会回收。

4.2.软引用(SoftReference)

内存空间足够,就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

4.3.弱引用(WeakReference)

垃圾收集器只要扫到,都会回收。

4.4.虚引用(PhantomReference)

被回收的时候会有一个通知,主要用来跟踪对象被垃圾回收的活动。

十一、高cpu占用优化

1.背景:

saas服务启动时,cpu占用太高。

2.过程:

  1. 首先确定CPU占比高的进程,线程ID。通过windows提供的工具Process Explorer可查到saas服务的进程ID(PID), 以及内部线程占用cpu情况。实时显示选中的进程内部线程ID,占用CPU情况。 操作saas,记录下占比CPU比较高的线程ID。
  2. 通过Jstack工具查看进程内部的线程执行情况: jstack -l PID > XXX.stack .
  3. 通过线程ID在jstack文件中找到消耗cpu比较大的线程为:C2 CompilerThread0 .在server模式下启动jar包后默认是开启tiered compiler对javac产生的字节码进行优化,这个线程消耗资源比较多。
  4.  用 -client 模式启动jar包后, 初步看内存占用从400+MB降到200+MB,CPU占比高的持续时间明显降低。

3.分析:

因为我们都知道JIT( just in time ), 也就是即时编译编译器。使用即时编译器技术,能够加速 Java 程序的执行速度。主要有Server 模式和 client 模式两种启动模式,主要的差别在于:-server 模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。原因是:当虚拟机运行在-client 模式的时候,使用的是一个代号为 C1 的轻量级编译器,而-server 模式启动的虚拟机采用相对重量级代号为 C2 的编译器。C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。

 

Hadoop

一、背景

一个任务:需要计算一个100M的文本文件中的单词的个数。写程序可以解决。

需要计算1T的文本文件汇总单词的个数,就需要有Hadoop了。

所以,简单说Hadoop就是存储海量数据和分析海量数据的工具。

二、Hadoop

Hadoop是Apache基金会开发的分布式系统基础架构。用java编写的,在分布式服务器集群上存储海量数据并运行分布式分析应用的开源框架,其核心部件是HDFS与MapReduce。

1.Hadoop框架的核心

HDFS:一个高度容错性的分布式文件系统,为海量的数据提供了存储。可以理解为一个分布式的,有冗余备份的,可以动态扩展的用来存储大规模数据的大硬盘。

MapReduce:为海量的数据提供了计算。可以理解成是一个计算引擎,按照MapReduce的规则编写Map计算/Reduce计算的程序,可以完成计算任务。

 

2.Hadoop作用

大数据存储:分布式存储

日志处理:擅长日志分析

数据挖掘:目前比较流行的广告推荐,个性化广告推荐

分布式事务框架

一、阿里开源框架Seata

1.正常提交

2.回滚

 

  • Seata实现2PC要点

1、全局事务开始使用 @GlobalTransactional标识 。

2、每个本地事务方案仍然使用@Transactional标识。

3、每个数据都需要创建undo_log表,此表是seata保证本地事务一致性的关键。

二、TCC模型

  • Try 阶段:对应 2PC 中一阶段的准备提交事务(Prepare);
  • Confirm 阶段:对应 2PC 中二阶段事务提交(Commit)。默认 Confirm 阶段是不会出错的,只要 Try 成功,Confirm 一定成功;
  • Cancel 阶段:对应 2PC 中二阶段事务回滚(Rollback)。

1.空提交

对于空提交,出现的情况为:第一阶段TM调用RM超时或者下游返回明确失败,此时TM仍旧调用下游执行提交的第二阶段,这对于RM来说,由于并没有Prepare阶段,然后收到了Commit请求,这就是一次空提交,对于TCC模型来说,空提交为系统的bug,需要业务进行处理。

2.空回滚

如图所示,TM在调用下游服务的一阶段Prepare操作时,因为超时而导致下游服务并没有收到请求,此时,TM会触发二阶段回滚操作,调用下游服务执行Rollback操作,因为下游服务在没有收到Prepare请求的情况下收到Rollback请求,这种场景被称为空回滚。 空回滚在实际使用中可能会出现,而且被视为正常情况,在处理业务逻辑时需要处理。

3.事务悬挂

如图所示,TM在调用下游服务的一阶段Prepare操作时,可能因为各种原因而TM没有收到下游响应,此时TM会执行Rollback操作。而对于下游来说,可能存在的一种情况是:下游服务先收到了Rollback请求,执行了空回滚操作,然后又收到了Prepare请求,然后执行了Try操作,此时,该下游服务的事务状态将永远存在于Prepare阶段,这种情况就叫做事务悬挂。 对于事务悬挂,目前处理的做法是设置事务悬挂检测程序,检测下游服务处于Prepare状态而上游服务处于Rollback的事务,调用下游服务执行二次Rollbacki操作。

三、基于消息中间件

1.A处理事务时,先向消息中间件发送一条消息,中间件进行消息持久化并返回应答; 2.A收到应答后开始处理并提交事务,提交成功后向消息中间件发送Commit请求(此时消息有可能丢失,如果发丢,由消息中间件的事务回查机制完成) 3.消息中间件收到Commit请求后便向B投递该消息; 4.B收到后开始处理事务,处理成功后向消息中间件返回应答 5.如果向B投递的消息未送达,则消息中间件重新投递该消息

通过以上流程实现了类2pc的流程,此时的消息中间件充当了TM的角色。

注:消息中间件有一个回查任务,定期扫描非最终态的消息,进行回查

四、其他

1.RocketMQ事务消息

执行流程:
1、Producer向Broker端发送Half Message;
2、Broker ACK,Half Message发送成功;
3、Producer执行本地事务(executeLocalTransaction);
4、本地事务完毕,根据事务的状态,Producer向Broker发送二次确认消息,确认该Half Message的Commit或者Rollback状态。Broker收到二次确认消息后,对于Commit状态,则直接发送到Consumer端执行消费逻辑,而对于Rollback则直接标记为失败,一段时间后清除,并不会发给Consumer。正常情况下,到此分布式事务已经完成,剩下要处理的就是超时问题,即一段时间后Broker仍没有收到Producer的二次确认消息;
5、针对超时状态,Broker主动向Producer发起消息回查(checkLocalTransaction);
6、Producer处理回查消息,返回对应的本地事务的执行结果;
7、Broker针对回查消息的结果,执行Commit或Rollback操作,同Step4。

 

 

RocketMQ

一、MQ对比

性能 ActiveMQ RabbitMQ RocketMQ Kafka
单机吞吐量 万级 万级 十万级 十万级
Topic数量对吞吐量的影响 Topic可以达到几百、几千个的级别,吞吐量会有小幅下降 Topic从几十个到几百个的时候,吞吐量会大幅下降
时效性 ms µs级 ms级 ms级以内
可用性 高,主从架构 高,主从架构 非常高,分布式架构 非常高,分布式架构
消息可靠性 有较低的概率丢失数据 经过参数配置,可以做到零丢失 经过参数配置,可以做到零丢失
负载均衡 支持 不支持 支持 支持
优势 非常成熟,功能强大,文档丰富,支持多语言 erlang语言开发,延时很低,管理界面友好,支持多语言,社区活跃 接口简单易用,分布式扩展方便,社区活跃,支持大规模的Topic,支持多种消费方式 极高的可用性和可靠性,分布式扩展方便
劣势 偶尔有较低概率丢失消息,社区活跃度不高 erlang语言开发不容易进行定制开发,集群动态扩展麻烦 只支持Java,接口不是按照标准JMS规范走的,有的系统迁移要修改大量的代码 大量topic下吞吐量降低。

RocketMQ和kafka对比:

性能:差不多,都支持10万级别。但是kafka在topic达到几百的时候性能下降严重,RocketMQ略微会有下降。

稳定性:都是分布式架构,稳定性较高。

可靠性:都可以做到零丢失

其他:RocketMQ支持消息重试、死信队列,kafka不支持,kafka主要用于大数据。

 

为什么选RocketMQ:高吞吐单机可以达10万,分布式架构,服务稳定,消息可靠,可以做到0丢失。

二、RocketMQ介绍

1.整体架构

 

 

Name Server:为 Producer 和 Consumer 提供路由信息。路由到Broker的信息。可以理解成是Broker的发现和注册。保存Topic和Broker的关系,即哪个Topic哪个Queue在哪个Broker上。

Producer:生产消息。

Consumer:消费消息。

Broker:存储消息的地方。

Topic:消息的逻辑分类,是生产者在发送消息和消费者在拉取消息的类别。

2.四种消息类型

  • 普通消息:没有什么特殊的地方,就是普通消息
  • 延迟消息:延迟特定的时间间隔后消息才会被消费者消费。目前只支持特定级别的延时消息(1s到2h多个级别),每一个延迟级别单独有一个定时器,定时(每隔1秒)拉取对应延迟级别的消费队列。
  • 顺序消息:对于指定的一个 Topic,Producer保证消息顺序的发到一个队列中,消费的时候需要保证队列的数据只有一个线程消费。
  • 事务消息:通过两阶段提交、状态定时回查来保证消息一定发到broker。

3.三种消息发送方式

消息发送方式 介绍 优点 缺点
同步发送 消息发出后,在收到接收方发回响应之后才发下一个数据包 简单 耗时高
异步发送 发出数据后,不等接收方发回响应,接着发送下个数据包。 (需要用户实现异步发送回调接口(SendCallback),在回调接口中对发送结果进行处理) 耗时短 需要实现SendCallback接口
单向发送 发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。 耗时短,微秒级别 可靠性会降低

4.消息发送流程

5.消息存储

  • CommitLog:存消息的文件(采用文件系统存储),单个Broker下的所有队列共用一个CommitLog。
  • ConsumeQueue:是消息消费队列文件,消息达到commitlog文件后将被异步转发到消息消费队列,供消息消费者消费;
  • IndexFile:消息索引文件,记录key和offset的对应关系。提供了一种可以通过key和时间区间来查询消息的方法。

总结一下:所有消息都记录在CommitLog,生产者生产完一个消息后,会dispatch到对应的某个ConsumeQueue中,然后消费者来当前Queue消费消息时,根据对于的偏移量到CommitLog找到对应的消息进行消费。IndexFile只是提供了可以根据key和对应时间来找对应消息的一种方法。

注:只要消息写入了CommitLog,那么这个消息就不会丢失了。

6.Consumer消费方式

有拉取式消费(Pull Consumer)以及推动式消费(Push Consumer)两种方式。pull就是消费者定时轮询Broker,push就是Broker收到消息时主动推送给消费者。

7.消息刷盘

同步刷盘:只有消息真正持久化到磁盘,Broker才会给Producer一个成功的ack响应。消息可靠性有保障,但是性能会有影响。

异步刷盘:只要消息写入PageCache即给Producer一个成功的ack响应,消息刷盘采用后台异步线程提交的形式进行。

三、常见问题

1.RocketMq快的原因

RocketMq与Kafka在写消息与发送消息上,继续沿用了Kafka的这两个方面:顺序写和零拷贝

1)顺序写

从磁盘读数据时,需要找到数据在磁盘上的地址(寻址),再进行读写,寻址需要的时间会比较长。但Kafka 的数据存储在磁盘上面,依然性能很好,这是因为,Kafka采用的是顺序写,直接追加数据到末尾。实际上,磁盘顺序写的性能极高,基本和内存速度一致,磁盘的顺序写这一机制,极大地保证了Kafka本身的性能
2)零拷贝
读取文件,再用socket发送出去这一过程,传统方式需要先读取再发送,会经过以下四次复制:
1、将磁盘文件,读取到操作系统内核缓冲区Read Buffer
2、将内核缓冲区的数据,复制到应用程序缓冲区Application Buffer
3、将应用程序缓冲区Application Buffer中的数据,复制到socket网络发送缓冲区
4、将Socket buffer的数据,复制到网卡,由网卡进行网络传输


第2次和第3次数据的复制过程,不仅没有任何帮助,反而带来了巨大的开销。使用零拷贝,就是说,直接由内核缓冲区Read Buffer将数据复制到网卡,省去第二步和第三步的复制。

注:RocketMq的存储消息的文件CommitLog的大小规定为1G。是因为零拷贝技术有限制,传输的文件不能超过2G。

2.消息有序是怎么实现的

全局有序:将Topic配置成只有一个MessageQueue队列。

局部有序:将有序的一组消息都存入同一个MessageQueue。(发送消息时可以指定MessageSelector对象,可以发到对应的MessageQueue)

注:因为有MessageQueue存在,消费者会从多个MessageQueue消费数据,所以不管全局有序还是局部有序,都是利用的MessageQueue的FIFO设计。

 

 

编程语言分类

一、语言类型

  • 机器语言:0和1的二进制代码

如:0000,0001,000000000001

  • 汇编语言:指令采用了英文缩写的标识符,更容易识别和记忆

如:mov edx, len

  • 高级语言:去掉了与具体操作有关但与完成工作无关的细节

如:printlnf(“hello world”)

二、高级语言分类

1.编译型:在程序执行之前,将程序源代码“翻译”成目标代码(机器语言),之后可以脱离其语言环境独立执行(编译后生成的可执行文件)。

优缺点:编译后程序运行时不需要重新翻译,直接运行,程序执行效率高,依赖编译器,跨平台性差些。如Go、C、C++、Delphi等

 

2.解释型:应用程序源代码一边由相应语言的解释器“翻译”成目标代码(机器语言),一边执行,效率比较低。

如Python、PHP、Ruby等语言。

注:Java既不属于传统的编译型语言,也不属于解释型语言,Java是先编译成“.class”字节码文件,然后再利用JVM虚拟机进行解释执行的,所以Java即可以说成编译型,也可以说成解释型。

雅思每日任务

一、个人

1.口语

  • 每天背三句
  • [背词+听+影子跟读]P1P2P3素材
  • P1、P2写答案并录音练习
  • [背+复述+模仿]P3的100道题目及重点词句
  • 复习课件
  • 8句话扩展每天练一遍

2.阅读

  • 背诵538同义替换 30分钟
  • 背诵阅读14天同替 20分钟
  • 做一套题
  • 复习课件,背诵对应积累的词

3.听力

  • 语料库50分钟(看三遍):95%正确率  (p0)
  • 点听:用c8或者c14  (p1)
  • 复听:用c8或者c14 (p2)
  • 做一套题:一步一步练,先练c11 12 13 15 的s1和s4,后练习s2、s3,分析题干和点复听。 (p2)
  • 考前一周集训:地图训练  (p1)
  • 背诵179同义替换 20分钟 (p2)
  • 复习课件

4.写作

  • 写一篇作文
  • 复习课件

二、官方

1.听力

  • 复习课件——最为重要——方法论一定要超级熟练背诵(中文+英文);8句话扩展每天练一遍
  • 每天背三句
  • P1P2P3素材背词、反复听、努力且不纠结地进行影子跟读
  • 评分标准解析视频课+做笔记视频课
  • 阅读文章练习
  • 准备并练习答题
    • P1不要背答案,自己练习录音回答,听自己的回答、改错、提升
    • P2不要背答案,可以直接录音回答,也可以把自己的答案写下来之后,再练习回答
    • P3练习初期可以背/复述/模仿我的回答,充分模仿我的回答(词汇、句型、思路)

 

三、资料

1.口语每天推8句话

  • what I love the most about my neighborhood is all the good amenities there
  • for me, the best way to relax is just hanging out with my parents
  • my primary school was so close to our house that I would just walk to school every day
  • I should definitely do more to protect the environment
  • we see advertisements everywhere
  • the house that I grew up in was kind of big
  • sitting in the back of the car appeals to me more cuz I can do my own thing back there
  • I haven’t planted any flowers, but I’ve kept some

从这八句话开始推,每天推一遍,每一天推出来的最好不一样