缓存穿透
:查询一个不存在的数据,Mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库。
(1)缓存空数据:数据库查询不到数据,将空结果缓存,并设置较短过期时间。简单,但消耗内存,可能会发生数据不一致的问题。
(2)布隆过滤器:缓存预热时,将信息提前存入布隆过滤器,后期添加数据时也存入布隆过滤器。内存占用较少,没有多余key,但实现复杂,存在误判。
位图(bitmap):相当于是一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1。
布隆过滤器作用:布隆过滤器可以用于检索一个元素是否在一个集合中。但可能存在误判,误判率一般在5%以内,数组越小误判率就越大,数组越大误判率就越小,但同时带来了更多的内存消耗,不支持元素的删除操作。实现方案:Guava、Redisson。
(3)请求参数校验:在应用层对请求参数进行有效性校验,过滤掉明显无效的请求。
(4)限制请求频率:对于频繁发送请求的用户,可以采用限制请求频率的策略。比如令牌桶算法限制每秒的请求数量。
缓存击穿
:当热key过期,同时有大量并发请求访问,可能会瞬间压垮数据库。
(1)互斥锁或分布式锁:缓存失效时,只允许一个请求查询数据并缓存。强一致,性能差。
(2)逻辑过期或永不过期:每次访问缓存时,判断缓存是否过期,若过期则新开线程同步数据,若期间有别的请求,则返回过期的数据。高可用,性能高。
缓存雪崩
:在同一时段大量缓存key同时失效或Redis服务宕机,导致大量请求到达数据库。
(1)分散缓存数据的过期时间:给不同的key的TTL(Time To Live,生存时间)添加随机值。
(2)降级限流:在缓存失效时,通过限制请求的并发数或采用熔断机制,控制对后端系统的访问压力,保护系统的稳定性,保底策略。如Nginx或Gateway。
(3)多级缓存:Guava或Caffeine做一级缓存,Redis做二级缓存。
(4)Redis集群提高服务可用性:哨兵模式、集群模式。
当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。
延迟双删
无论先删除缓存还是先修改数据库,都有可能导致数据不一致(脏数据),因此需要再删除一次缓存(降低脏数据的出现),延时是为了将数据同步到从数据库(延时时间不好确定),也有脏数据的风险。
分布式锁-Redisson读写锁(强一致性)
共享锁:读锁(readLock),加锁之后,其它线程可以共享读操作。
排他锁:独占锁(writeLock),加锁之后,阻塞其它线程读写操作。
RDB
:RDB 是 Redis 的一种数据快照持久化方式,它将 Redis 内存中的数据都保存到磁盘上(dump.rdb)。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。手动备份:save(会阻塞Redis服务器)、bgsave(后台异步保存快照)。自动备份:修改redis.conf文件,save 900 1(900秒内,如果至少有一个key被修改,则执行bgsave)。
RDB执行原理:bgsave开始时会fork(克隆)主进程得到子进程,子进程共享主进程的内存数据(只克隆页表,可用来访问物理内存)。完成fork后读取内存数据并写入RDB文件,fork采用copy-on-write技术避免了脏写(当主进程执行写操作时,会拷贝一份数据,执行写操作)。
AOF
:Append Only File(追加文件),Redis处理的每一个写命令都会记录在AOF文件,可以看作是命令日志文件。AOF默认是关闭的,需要修改redis.conf配置文件开启AOF、AOF文件的同步策略、AOF重写(bgrewriteaof命令,控制 AOF文件的大小并提高恢复速度,去除过期的键和无效的命令)。
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
# AOF文件比上次文件增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
惰性删除 + 定期删除两种策略进行配合使用。
当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
(1)noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
(2)volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
(3)allkeys-random:对全体key,随机进行淘汰。
(4)volatile-random:对设置了TTL的key,随机进行淘汰。
(5)allkeys-lru
:对全体key,基于LRU算法(Least Recently Used,最近最少使用)进行淘汰。
(6)volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰。
(7)alkeys-lfu:对全体key,基于LFU算法(Least Frequently Used,最少频率使用)进行淘汰
(8)volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰
使用场景:集群情况下的定时任务、抢单、幂等性场景。
SETNX
:SET if not exists,当键不存在时才设置键的值(对应Java中调用setIfAbsent)。必须设置过期时间,因为可能Redis服务器宕机导致锁永远无法释放,即死锁。
(1)设置过期时间:SETNX lock value EX 10,过期时间过长影响性能,过期时间短可能被其它线程抢走锁进而重复操作数据库。
(2)手动删除锁:Lua保证原子性,先判断锁是否是自己的(避免过期了删除其它线程的锁)再删除,但可能判断锁是自己的了然后CPU执行别的了,又过期了然后删除了别的锁。
Redisson
:看门狗机制续期(默认每10秒就会重置过期时间为30秒,一个新的线程来监听),释放锁时就通知看门狗结束监听。同时加锁、设置过期时间都是基于Lua脚本完成,保证原子性。可重入(同一个线程可以多次获得同一个锁):利用hash结构记录线程id和重入次数;主从一致性:可使用RedLock(红锁)解决,至少在(n / 2 + 1)个Redis实例上创建锁,但性能太低。Redis整体是AP思想(高可用性),可利用Zookeeper实现分布式锁来保证强一致性(CP思想)。
@Autowired
private RedissonClient redissonClient; // 自动注入Redission
RLock lock = redissonClient.getLock("myLock"); // 获取分布式锁对象
lock.lock(); // 获取分布式锁,默认30秒;或者用trylock()方法指定最长重试时间
try {
// 执行需要互斥访问的任务
// ...
} finally {
lock.unlock(); // 释放锁
}
主从复制
Redis一主多从集群,主从复制和读写分离,保证了Redis的高并发。
(1)主从全量同步
Replication Id
:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid。
offset
:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
(1)从节点请求主节点同步数据(replication id、 offset )
(2)主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
(3)主节点执行bgsave,生成rdb文件后,发送给从节点去执行
(4)在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
(5)把生成之后的命令日志文件发送给从节点进行同步
(2)主从增量同步(slave重启或后期数据变化)
(1)从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
(2)主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
哨兵模式
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复,进而保证Redis的高可用。
(1)监控:Sentinel会不断检查master和slave是否按预期工作,Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令。
主观下线:如果某 sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
(2)自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。哨兵选主规则:首先判断主与从节点断开时间长短,如超过指定值就排除该从节点;然后判断从节点的slave-priority值,越小优先级越高;如果slave-prority一样,则判断slave节点的offset值
,越大优先级越高;最后是判断slave节点的运行id大小,越小优先级越高。
(3)通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端。
Redis集群脑裂
问题:当Redis分布式集群中出现网络分区或通信故障时,导致集群中的不同子集出现独立运行的情况,进而产生两个master,而客户端仍向旧的master写入数据,新的master无法同步数据,导致数据丢失。以下两种方法可以避免大量的数据丢失。
(1)设置最少的从节点数量:min-replicas-to-write 1 表示master节点最少有一个slave节点才会写入数据,否则拒绝。
(2)缩短主从数据同步延迟时间:min-replicas-max-lag 5 表示主从数据同步的延迟不能超过5秒才写入数据,否则拒绝。
分片集群
集群中有多个master,每个master保存不同数据(解决海量数据存储、高并发写问题);每个master都可以有多个slave节点(解决高并发读问题);master之间通过ping监测彼此健康状态;客户端请求可以访问集群任意节点,最终都会被路由转发到正确节点。
Redis分片集群引入了哈希槽
的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
(1)String字符串:缓存(验证码、token)、计数(点赞量、播放量)。
(2)Hash散列表:哈希表;存储对象(用户属性)。
(3)List列表:双向链表,有序,增删快,查询慢;消息队列。
(4)Set无序集合:哈希表,键值对无序,唯一,支持交/并/差集操作;独立IP,共同爱好,标签。
(5)Sorted Set有序集合:跳表
(当有序集合元素数量小于512且每对key和value小于64字节时用压缩列表);键值对有序,唯一,自带权重分数score;排行榜。
压缩列表:用于在某些情况下代替普通的双向链表、哈希表、跳跃表,以节省内存空间和提高性能。
跳表:增加了多级索引,通过多级索引位置的跳转,实现了快速查找元素。范围查找:跳表效率比红黑树高;且跳表实现比红黑树简单,易懂。
出现慢查询原因:聚合查询、多表查询、表数据量过大查询、深度分页查询等。表象:页面加载过慢、接口压测响应时间过长(超过1s)。
(1)开源工具:调试工具(Arthas);运维工具:(Prometheus、Skywalking)
(2)慢查询日志:记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。默认没有开启,在MySQL的配置文件(/etc/my.cnf)中配置:slow_query_log=1来开启慢日志查询 和long_query_time=2自定义执行时间。在localhost-slow.log日志文件中查看具体信息。在调试阶段开启,在生产模式开启会损耗Mysql的性能。
(1)possible_keys:当前sql可能用到的索引。
(2)key:当前sql实际用的索引。
(3)key_len:索引占用的大小。
(4)extra:额外的优化建议。Using where; Using Index:查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据;Using index condition:查找使用了索引,但是需要回表查询数据。
(5)type
:连接类型,性能由好到差的连接类型为NULL(没有访问表或索引)、system(查询系统表)、const(主键查询)、eq_ref(主键索引查询或唯一索引查询)、ref(非唯一性索引,即多行)、range(范围查询)、index(索引树扫描)、all(全表扫描,不走索引)。最低要求为range。
索引(Index)是帮助MySQL 高效获取数据的数据结构(有序)。提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)。通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。MySql的InnoDB引擎默认采用B+树来存储索引,层级少,搜索效率高;非叶子节点只存储指针,叶子节点存储数据;叶子节点是双向循环链表连接的,便于范围查询。
聚簇索引(聚集索引)与非聚簇索引(二级索引)
(1)聚簇索引:将数据存储和索引放到了一块,索引结构的叶子结点保存了行数据。必须有,且只有一个。如果主键存在,主键索引就是聚簇索引;如果主键不存在,则使用第一个唯一索引作为聚簇索引;如果既没有主键,也没有唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚簇索引。
(2)非聚簇索引:将数据与索引分开存储,索引结构的叶子结点关联的是对应的主键,即二级索引会先通过自己的B+树找到要查询数据的主键,然后再根据主键从聚簇索引中查询对应的行数据,即回表查询
。
覆盖索引:查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到,否则需要回表查询(尽量避免select *查询)。
MySql超大分页:在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。优化思路:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。
索引创建原则
(1)针对于数据量较大,且查询比较频繁的表建立索引。单表超过10万数据(增加用户体验)
(2)针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
(3)尽量选择区分度高的列作为索引(索引列上不同值的数量与总行数之比),尽量建立唯一索引,区分度越高,使用索引的效率越高。
(4)如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
(5)尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
(6)要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
(7)如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
索引失效
(1)最左前缀法则:对于联合索引来说,索引的最左列必须存在(与位置无关),若中间有某列索引跳过(即不存在),则只有前边的列索引才有效。
(2)范围查询:在联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效。规避方法:尽量使用>=或<=。
(3)不要在索引列上进行运算操作,否则索引将失效。
(4)字符串不加引号,索引将失效(由于Mysql优化器会自动的进行类型转换)。
(5)以%开头的like模糊查询,索引失效。如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。
(1)drop:drop table 表名;
属于DDL,删除整张表,包含行数据、字段、索引、约束等,释放磁盘空间,无法找回。
(2)truncate:truncate table 表名;
属于DDL,删除表的全部数据(本质是先删除表,后创建,但进行了优化),释放磁盘空间,无法找回,重置自增列。
(3)delete:delete from 表名 [where ...];
属于DML,删除表的部分或所有数据,走事务,一行一行执行删除,且会记录日志,可以恢复删除的数据。在InnoDB引擎中,并不是真的删除数据,而是给数据打上删除标记,标记为删除状态,磁盘空间不会释放(可通过optimize table 表名;
进行释放),且不会重置自增列。
执行速度:drop > truncate >> delete
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。如银行转账。
事务四大原则(ACID)
(1)原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。undo log
(2)一致性(Consistency):事务完成时,必要使所有的数据都保持一致状态。redo log + undo log
(3)隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。MVCC + 锁
(4)持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。redo log
事务并发问题
(1)脏读:一个事务读到另外一个事务还没有提交的数据。
(2)不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同。
(3)幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了“幻影”(前提是已经解决了不可重复读的问题)。
undo log 和 redo log
缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。
redo log
:重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改物理信息都会存到该日志中,用于在刷新脏页到磁盘时,发生错误时,进行数据恢复使用。
undo log
:回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制)。undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。undo log 保证了事务的原子性
,redo log + undo log 保证了事务的一致性
。当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。而update、delete的时候,产生的undo log日志不仅在回滚时需要,MVCC版本访问也需要,不会立即被删除。
MVCC(多版本并发控制)
多版本并发控制:指维护—个数据的多个版本,使得读写操作没有冲突。
(1)三个隐藏字段
(2)undo log版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
(3)readview
当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读:简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。Read Committed:每次select,都生成一个快照读;Repeatable Read:开启事务后第一个select语句才是快照读的地方;Serializable:快照读会退化为当前读。
readview
:读视图是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的) id。
read committed:在事务中每一次执行快照读时生成ReadView。(即读取的数据应该是已经提交的事务)
repetable read:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
(1)Master主库在事务提交时,会把数据变更(DDL和DML)记录在二进制日志文件BinLog中。
(2)从库(IOThread)读取主库的二进制日志文件BinLog,写入到从库的中继日志Relay Log。
(3)从库slave重做(SQLThread)中继日志中的事件,将改变反映它自己的数据。
单表数据超过1000万或20G
(1)垂直分库:以表为依据,根据业务将不同表拆分到不同库中,高并发下提高磁盘IO和网络连接数。
(2)垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中,将冷热数据分离,多表互不影响。
(3)水平分库:将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题。
(4)水平分表:将一个表的数据拆分到多个表中,解决单表存储和性能问题。
(1)shardingJDBC:基于AOP原理,在应用程序中对本地执行的SQL进行拦截,解析、改写、路由处理。需要自行编码配置实现,只支持java语言,性能较高。
(2)MyCat:数据库分库分表中间件,不用调整代码即可实现分库分表,支持多种语言,性能不及前者。
Bean线程安全问题
单例Bean存在线程问题(@Scope注解可以设置Bean的作用域),主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。大部分Bean实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下,Bean是线程安全的。
(1)在Bean中尽量避免定义可变的成员变量。
(2)在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中。
(3)多例、加锁。
Spring AOP
AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为切面(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
(1)记录操作日志:定义切面类(实现对日志的记录),切点表达式为一个自定义注解,这样凡是用改注解的方法都要记日志。
(2)缓存处理:Spring Cache框架,@EnableCaching(开启缓存注解)、@Cacheable(缓存不存在再调用方法并存入缓存)、@CachePut(方法返回值放入缓存)、@CacheEvict(删除缓存)
(3)Spring内置的事务处理(声明式事务),对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或回滚事务。
事务失效
(1)异常捕获失效:事务通知必须自己捕获到抛出的异常才会回滚,若目标自己处理则失效,因此需要抛出异常。
(2)抛出检查(非运行时)异常:Spring默认只会回滚非检查异常,可以@Transactional(rollbackFor=Exception.class)设置捕获的异常类型。
(3)非public修饰的方法
(4)非事务方法调用事务方法、事务方法调用事务方法(不使用代理对象时会失效)
Bean生命周期
(1)通过BeanDefinition获取bean的定义信息
(2)调用构造函数(无参构造)实例化bean
(3)bean的依赖注入(set方法)
(4)处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware),用于获取BeanName、BeanFactory、ApplicationContext等资源。
(5)Bean的后置处理器BeanPostProcessor-前置
(6)初始化方法(InitializingBean接口、init-method属性)
(7)Bean的后置处理器BeanPostProcessor-后置
(8)销毁bean(DisposableBean接口、 destroy-method属性)
循环依赖
(1)一级缓存(singletonObjects):单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象。
(2)二级缓存(earlySingletonObjects):缓存早期的bean对象(生命周期还没走完)。
(3)三级缓存(singletonFactories):缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的(可以是代理对象)。
若构造方法发生循环依赖,可以通过@Lazy
注解进行懒加载。
Spring事务原理
(1)解析切面:Bean初始化前的后置处理器中解析advisor(pointcut(通过@Transacational解析的切点),advise)(这个advisor是通过@EnableTransactionManagement注册了一个配置类,该配置类就配置了adivsor)
(2)创建动态代理:Bean初始化后的后置处理器中创建动态代理(有接口的JDK,没接口的cglib),创建动态代理之前会先根据advisor中pointCut匹配@Transacational(方法、类),匹配到就创建动态代理。
(3)调用:动态代理
try {
创建一个数据库连接Connecton,并且修改数据库连接的autoCommit属性为false,禁止此连接的自动提交。
然后执行目标方法,方法中会执行数据库操作sql。
}
catch {
如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
}
(4)执行完当前方法后,如果没有出现异常就直接提交事务。
Spring多线程事务能否保证事务的一致性(同时提交、同时回滚)?
Spring的事务信息是存在ThreadLocal中的Connection,所以一个线程永远只能有一个事务;可以通过编程式事务或分布式事务。
@SpringBootApplication注解
(1)@SpringBootConfiguration:与@Configuration注解作用相同,声明当前也是配置类。
(2)@ComponentScan:组件扫描,默认扫描当前引导类所在包及其子包。
(3)@EnableAutoConfiguration
:SpringBoot实现自动化配置的核心注解。该注解通过@Import
注解导入对应的配置选择器,会读取该项目和该项目引用的jar包的classpath路径下META-INF/spring.factories
文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解(如@Conditional
的子类@ConditionalOnClass
(是否有字节码文件)、@ConditionalOnMissingBean
(是否有对应的Bean)、@ConditionalOnProperty
(是否有对应的属性和值)等)所指定的条件来决定是否需要将其导入到Spring容器中。
SpringBoot内置Tomcat启动原理
(1)当加载spring-boot-starter-web依赖时会在SpringBoot中添加:ServletWebServerFactoryAutoConfiguration,servlet容器自动配置类。
(2)该自动配置类通过@Import导入了可用(通过@ConditionalOnClass判断决定使用哪一个)的一个Web容器工厂(默认Tomcat)。
(3)在内嵌Tomcat类中配置了一个TomcatServletWebServerFactory的Bean (Web容器工厂)。
(4)它会在SpringBoot启动时加载ioc容器(refresh)OnRefersh创建内嵌的Tomcat并启动。
Spring、SpringMVC、SpringBoot区别
(1)Spring是一个分层的轻量级开源框架,核心是控制反转(IOC)和面向切面编程(AOP),可以有多种Web层、业务层、持久层配置方案,可以配置Bean和维护Bean之间的关系。
(2)SpringMVC是Spring的子类,是Spring对Web层的一种解决方案,用于处理请求和响应请求。
(3)SpringBoot简化了Spring的XML配置,约定大于配置的设计理念,自动配置和启动类。
(1)Spring常用注解
(2)SpringMVC常用注解
(3)SpringBoot常用注解:见3.3
Mybatis执行流程
(1)读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
(2)构造会话工厂SqlSessionFactory
(3)会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
(4)操作数据库的接口,Executor执行器,同时负责查询缓存的维护
(5)Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
(6)输入参数映射
(7)输出结果映射
Mybatis延迟加载
延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true/false,默认是关闭的。
(1)使用CGLIB创建目标对象的代理对象。
(2)当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,执行sql查询。
(3)获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了。
Mybatis一级、二级缓存
一二级缓存都是基于PerpetualCache的HashMap(本地缓存)实现的。
(1)一级缓存:其存储作用域为Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。
(2)二级缓存:其存储作用域为namespace和mapper,不依赖于SQL Session,默认是关闭的。
对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有select中的缓存将被clear;二级缓存需要缓存的数据实现Serializable接口;只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中。
(1)注册中心:Nacos(配置中心)、Eureka
(2)负载均衡:Ribbon、LoadBalancer
(3)服务调用:Feign
(4)服务保护:Sentinel
(5)服务网关:Gateway
Eureka
(1)服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、ip、端口等。
(2)服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用。
(3)服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没接收到心跳,从eureka中剔除。
Nacos
(1)临时实例:心跳检测,若Nacos检测出服务停止,就直接剔除该临时实例。
(2)非临时实例:Nacos主动询问,若Nacos检测出服务停止,仍会不断去检测该服务是否可用。
(3)Nacos支持服务列表变更的消息推送模式,服务列表更新更及时。
(4)Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式。
(1)RoundRobinRule:简单轮询服务列表来选择服务器。
(2)WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小。
(3)RandomRule:随机选择一个可用的服务器。
(4)BestAvailableRule:忽略那些短路的服务器,并选择并发数较低的服务器。
(5)RetryRule:重试机制的选择逻辑。
(6)AvailabilityFilteringRule: 可用性敏感策略,先过滤非健康的,再选择连接数较小的实例
(7)ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
自定义负载均衡策略:自定义类实现IRule接口,再通过配置类(全局,@Bean注解)或者配置文件配置(局部,某服务)即可。
CAP理论
(1)一致性(Consistency):在分布式系统中的所有节点,无论客户端访问哪个节点,都能获得相同的数据副本和最新的数据。即系统在数据更新后,所有的节点都会同步更新。
(2)可用性(Availability):在分布式系统中,每个请求都能够得到响应,无论系统是否遇到故障或部分节点失效。
(3)分区容错性(Partition tolerance):分布式系统能够继续工作,即使网络中的某些节点之间出现了通信故障或者分区(网络分割)。
分布式系统节点之间肯定是需要网络连接的,分区(P)是必然存在的。
如果保证访问的高可用性(A),可以持续对外提供服务,但不能保证数据的强一致性–>AP
如果保证访问的数据强一致性(C),就要放弃高可用性–>CP
BASE理论
(1)Basically Available (基本可用)︰分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
(2)Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
(3)Eventually Consistent(最终一致性)∶虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP)
强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚(CP)
分布式事务
(1)Seata框架(XA、AT、TCC)
TC (Transaction Coordinator)事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
TM(Transaction Manager)事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
RM (Resource Manager)资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
XA
:保证强一致性,CP模式,数据互相等待提交或回滚(资源锁定)
AT
:保证高可用性,AP模式,执行sql就提交,通过undo log回滚
TCC
:保证高可用,AP模式,但要手动实现,耦合度高
(2)MQ消息队列
(3)任务调度
幂等性:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。如用户重复点击(网络波动)、MQ消息重复、使用失败或超时重试机制。
(1)数据库唯一索引:可保证新增是幂等的。
(2)token+redis:可保证新增和修改都是幂等的。
(3)分布式锁:可保证新增和修改都是幂等的,性能较差。
xxl-job分布式任务调度
解决集群任务的重复执行问题;cron表达式定义灵活;定时任务失败可以重试和统计;任务量大则可分片执行。
(1)xxl-job路由策略
第一个;最后一个;轮询;随机;一致性HASH(哈希散射);LRU(最近最久未使用);LFU(最小频率使用);故障转移(按顺序心跳检测,第一个心跳检测成功的机器选为目标执行器);忙碌转移(检测第一个空闲的);分片广播
(广播给所有的执行器执行任务)。
(2)xxl-job任务执行失败
故障转移(路由策略) + 失败重试, 查看日志分析 ---- > 邮件告警
(3)大数据量任务同时执行
执行器群部署时,任务路由策略选择分片广播情况下,一次任务调度将会广播触发对应集群所有执行器执行一次任务。在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行。
消息不丢失
(1)RabbitMQ提供了生产者确认机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。回调方法即时重发、记录日志、定时重发。
(2)MQ默认是内存存储消息,开启持久化功能可以确保缓存在MQ中的消息不丢失。交换机持久化、队列持久化、消息持久化。
(3)RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。而SpringAMQP允许三种确认机制,可以利用Spring的retry机制,在消费者出现异常时利用本地重试,设置重试次数,当次数达到了以后,如果消息依然失败,则将消息投递到异常交换机,交由人工处理。
manual:手动ack,需要在业务代码结束后,调用api发送ack。
自动ack,有spring检测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack。
关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除。
消息重复消费
当消费者还没有给MQ确认时,服务宕机,导致服务重启之后,又消费了一次消息,即重复消费。
消息唯一标识:在消息体中添加一个全局唯一的标识符(例如UUID),消费者处理消息时先检查标识符是否已经处理过,避免重复消费。
幂等性处理:设计消费者的处理逻辑为幂等操作,即无论收到相同的消息多少次,处理的结果都保持一致。这样即使消息被重复消费,也不会影响最终的处理结果。
死信交换机(延迟队列)
延迟队列:进入队列的消息会被延迟消费的队列。场景:超时订单、显示优惠、定时发布。延迟队列=死信交换机+TTL(生存时间)。
死信交换机:当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter) :
(1)消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
(2)消息是一个过期消息,超时无人消费
(3)要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)。
TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信。TTL超时分为两种情况:消息所在的队列设置了存活时间;消息本身设置了存活时间。
延迟队列插件:DelayExchange插件,安装在RabbitMQ中。在exchange配置中添加属性delayed="true"即可设置为实现延迟队列的交换机;发消息时.setHeader(“x-delay”, 10000) 来设置消息的TTL。
消息堆积
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。
(1)增加更多消费者,提高消费速度
(2)在消费者内开启线程池加快消息处理速度
(3)扩大队列容积(惰性队列),提高堆积上限
惰性队列:接收到消息后直接存入磁盘而非内存;消费者要消费消息时才会从磁盘中读取并加载到内存;支持数百万条的消息存储。
RabbitMQ高可用机制
(1)普通集群:会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回队列。所在节点宕机,队列中的消息就会丢失
(2)镜像集群:本质是主从。交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份;创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。一个队列的主节点可能是另一个队列的镜像节点;所有操作都是主节点完成,然后同步给镜像节点;主宕机后,镜像节点会替代成新的主节点。
(3)仲裁队列:与镜像队列一样,都是主从模式,支持主从数据同步使用非常简单,没有复杂的配置,主从同步基于Raft协议,强一致。
JMM:Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
(1)主内存:主内存是所有线程共享的内存区域,其中存储了所有的变量和对象。Java程序在运行时,所有线程都可以读写主内存中的数据。
(2)工作内存:每个线程都有自己的工作内存,用于保存从主内存中读取的变量的副本。线程在执行过程中,只能直接操作工作内存中的数据。
(3)内存间交互:线程在执行时,会将主内存中的变量值复制到工作内存中进行操作,并在操作完成后,将变量值写回主内存。这个过程涉及到变量的读、写和刷新等操作。
(4)顺序一致性:JMM要求所有线程对共享变量的操作都必须符合顺序一致性的原则。即对一个变量的所有写操作必须对其他线程可见,而且所有操作的顺序必须遵循程序代码的先后顺序。
(5)synchronized和volatile:synchronized保证多线程访问共享资源时的互斥性(原子性),volatile保证共享变量的可见性和有序性(禁止指令重排,JIT即时编译器会优化)。
volatile可见性与有序性
(1)可见性:volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。
(2)有序性:用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。技巧:写变量让volatile修饰的变量的在代码最后位置;读变量让volatile修饰的变量的在代码最开始位置。
AQS
:AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。如ReentrantLock(阻塞式锁)、Semaphore(信号量)、CountDownLatch(倒计时锁)。
(1)AQS基本工作机制
(2)AQS如何保证原子性:在多个线程修改state时通过CAS保证的原子性。
(3)AQS既可实现是公平锁,也可实现非公平锁:新的线程与队列中的线程共同来抢资源,是非公平锁;新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁。
死锁
当一个线程需要同时获取多把锁,就容易发生死锁。
(1)jps:输出JVM种运行的进程状态信息。
(2)jstack:查看java进程内线程的堆栈信息(jstack -l 进程号)。
(3)jconsole:用于对jvm的内存,线程,类的监控。
(4)VisualVM:故障处理工具,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈。
ConcurrentHashMap
(1)JDK1.7:分段数组+链表,底层用ReentrantLock,锁一段数组,粒度较大。
(2)JDK1.8:数组+链表/红黑树,CAS添加新节点,采用synchronized锁住链表/红黑树首节点,细粒度,性能更好。
线程池核心参数与执行原理
ThreadPoolExecutor(int corePoolSize, //核心线程数,不能小于0
int maximumPoolSize, //最大线程数,不能小于0且大于等于核心线程数
long keepAliveTime, //临时线程最多不工作的时间,超过就会销毁,不能小于0
TimeUnit unit, //临时线程最多不工作的时间单位,如TimeUnit.SECONDS
BlockingQueue<Runnable> workQueue, //任务队列,即阻塞队列,不能为null
ThreadFactory threadFactory, //线程工厂,如Executors.defaultThreadFactory(),不能为null
RejectedExecutionHandler handler);//任务拒绝策略,如new ThreadPoolExecutor.AbortPolicy(),静态内部类,不能为null
线程池种常见阻塞队列
(1)ArrayBlockingQueue:基于数组结构的有界阻塞队列(循环队列),FIFO。
(2)LinkedBlockingQueue:基于链表结构(单向链表)的有界阻塞队列,FIFO。
核心线程数
(1)IO密集型的任务→(CPU核数*2+1)
(2)计算密集型任务→(CPU核数+1)
线程池种类
(1)固定线程数:ExecutorService threadPool = Executors.newFixedThreadPool(3);无临时线程,阻塞队列用的Linked,适用于任务量已知,相对耗时的任务。
(2)单线程化:ExecutorService executorService = Executors.newSingleThreadExecutor();只有一个核心线程,阻塞队列用的Linked,适用于固定顺序的任务。
(3)可缓存:ExecutorService threadPool = Executors.newCachedThreadPool();核心线程数为0,最大线程数为Integer.MAX_VALUE,适合任务书比较密集,但每个任务执行时间较短的情况。
(4)“延迟”和“周期执行”功能的ThreadPoolExecutor:ExecutorService threadPool = Executors.newScheduledThreadPool(2);
不建议使用Executors创建线程池
阿里巴巴开发手册-嵩山版,禁止使用Executors创建线程池,而是使用new ThreadPoolExecutor()自定义线程池。由于阻塞队列是无界的或临时线程数是无界的,可能导致OOM。
CountDownLatch(闭锁/倒计时锁)
用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)。其中构造参数用来初始化等待计数值;await()用来等待计数归零(可以设置最长等待时间);countDown()用来让计数减一。
线程池使用场景
(1)ES数据批量导入、任务调度执行器:线程池+CountDownLatch,防止内存溢出
(2)数据汇总:当使用多接口来汇总数据时(接口间没有依赖或部分依赖),可使用线程池+Future来提升性能
(3)异步调用:为避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可使用线程池新启一个线程异步执行(@Async),提升方法响应时间
控制并发线程数
Semaphore:通常用于那些资源有明确访问数量限制的场景,常用于限流。创建时执行信号量个数;acquire()请求信号量;release()释放信号量。
ThreadLocal
ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。set()设置值、get()获取值、remove()清除值。ThreadLocal本质来说就是一个线程内部存储类
(内部类ThreadLocalMap来存储数据,为每一个线程都维护了一个entry数组存储数据,key为ThreadLocal(由于Entry继承了WeakReference,因此是弱引用,会被GC释放,而value是强引用,因此会造成内存泄漏,解决方法:务必remove清除值),value为资源值),从而让多个线程只操作自己内部的值,从而实现线程数据隔离。
CAS
:AbstractQueuedSynchronizer(AQS框架)、AtomicInteger、AtomicStampedReference(版本号解决ABA问题)、AtomicMarkableReference(布尔类型解决ABA问题),也可以通过时间戳解决ABA问题。CAS底层依赖于Unsafe类
来直接调用操作系统底层的CAS指令。 可通过jol
依赖提供的ClassLayout.parseInstance(锁对象).toPrintable()方法打印锁对象的堆内存信息。
线程ID
记录在对象头的MarkWord中,并将对象标记为偏向锁状态,只会在第一次执行CAS,重入时只判断线程id是否是自己,不是则有线程竞争升级为轻量级锁。Lock Record锁记录
,将其Object reference字段指向锁对象;通过CAS指令将Lock Record的地址和对象头MarkWord进行互换;若当前线程已持有该锁,则锁重入,该线程仍会创建一个锁记录,不过第一部分为null;若CAS修改失败,说明发生了竞争。解锁时,找到所有指向该锁对象的锁记录,为null则删除锁记录,非null则利用CAS指令交换对象头的MarkWord为原来状态。竞争的线程通过自旋
来不断尝试获取锁,让线程在等待时不会被挂起,减少了用户态和内核态的频繁切换,当自选次数超过阈值,则锁膨胀为重量级锁。Monitor
实现的(monitorenter/monitorexit指令),重量级锁是一种传统的互斥锁,它会使等待获取锁的线程阻塞,并将线程调度到内核态,进入等待队列。Synchronized:底层由JVM提供的由C++实现的Monitor监视器实现(对象锁会与Monitor进行关联,该对象头的MarkWord指向Monitor对象)。Monitor实现的锁属于重量级锁(Mutex Lock),里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
(1)Owner:存储当前获取锁的线程,只能有一个线程可以获取。
(2)EntryList:关联没有抢到锁的线程,处于Blocked状态的线程(任何线程都有可能强到锁)。
(3)WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程。
(4)HoldCount:持有计数器,确保同一个线程在持有锁的过程中可以再次获取相同的锁。
(5)同样会存储对象头中markword中的数据(比如当调用hashCode方法时,会根据指向的Monitor对象查找)。
ReentrantLock:可重入锁(持有计数器),底层由CAS+AQS队列实现,支持公平锁(有参构造true,效率低)和非公平锁(默认)。可中断、设置超时时间、设置公平锁、支持多个条件变量。
Synchronized与Lock
(1)语法层面:synchronized是关键字,源码在jvm 中,用C++语言实现;Lock是接口,源码由jdk提供,用java语言实现;使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁。
(2)功能层面:二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能;Lock提供了许多synchronized不具备的功能,例如公平锁、可打断(lockInterruptibly)、可超时(tryLock)、多条件变量(newCondition);Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)
(3)性能层面:在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖;在竞争激烈时,Lock的实现通常会提供更好的性能。