Java面试八股文

 一.MySql篇

1优化相关问题

1.1.MySql中如何定位慢查询?


 慢查询的概念:在MySQL中,慢查询是指执行时间超过一定阈值的SQL语句。这个阈值是由long_query_time参数设定的,它的默认值是10秒。也就是说,如果一条SQL语句的执行时间超过了long_query_time所设定的时间,那么这条SQL语句就被认为是慢查询。

c3a3726f388449af91dccc16e1488685.png

现在的问题就是:出现页面加载过慢,响应时间过长。我怎么确定就是我sql的问题呢?就算是sql的问题,我应该怎么找出执行慢的那条sql语句呢?

方案一:使用开发工具

72288bd0cb944803b12af50d739af44d.png

以Skywalking为例。这个工具,可以直观看到每个接口的执行时间。还可以追踪进接口,详细的看耗时情况,其中也包含了sql的耗时。

方案二:使用MySql自带的慢日志

简单来说,这个日志记录了执行时间较长的sql。

f50832f0c6f14198a419d7bc69f3da15.png

总结:

ad58f229d8c44c6a9033fe9cad8e797c.png

1.2.索引相关概念


基础概念:

  索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。在MySql5.5之后,InnoDB就是默认的存储引擎,因此我们这里说的都是InnoDB的存储引擎。  

        在MySQL中,索引是一种数据结构,用于加快数据库查询的速度和性能。索引可以帮助MySQL快速定位和访问表中的特定数据,就像书籍的索引一样,通过存储指向数据行的指针,可以快速查找到需要的页面。

底层数据结构 (B+树):

b4fec24af3144061906a6e5c201d11e7.png

        1.磁盘读写代价更低:非叶子节点不存放数据,只存储指针,相对来说存储压力低。数据只存储在叶子节点,在查询比对的时候,就不会把非叶子节点上的数据也加载出来了。

        2.查询效率稳定:因为数据都存储在非叶子节点,在查询时都要从根节点开始对比。最终到叶子节点获取数据。

        3.便于扫库和区间查询:叶子节点之间采用双向指针。在范围查询时更加方便。比如说:我们现在要查找索引为6~34区间的数据。先从根节点出发找到比38小的16,再从16往左找到叶子节点6,由于叶子节点之间有双向指针,因此6~34区间的数据都能获取!(这个区间内的数据不需要再从根节点再次查找)

991aa9fbcbd94df6b466b10cf7d28c0d.png

总结:

1.什么是索引:

        索引在项目中还是比较常见的,它是帮助MySQL高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的IO成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU的消耗。
2.索引底层的数据结构:
        MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是B+树便于扫库和区间查询,叶子节点是一个双向链表。
3.B树和B+树的区别:
        第一:在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定。
第二:在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表。

1.3.聚簇索引和非聚簇索引


8fff9a9a2d2c4681951cb138496307c8.png

1c88f6c8cd7c47b5bffc4d6406256e18.png

这里的主键id就是聚集索引,每个叶子节点存放的数据,就是当前叶子节点主键id所对应的一行数据!

假如我们这里把name字段也添加了索引,它就是非聚集索引 (二级索引)。它的叶子节点所存储的数据,就是当前name对应的主键id。比如说叶子节点Arm,存储的数据就是:10。

1.4回表查询


327bd1a851c44324ad2e028d342b2654.png

现在我们有一条sql语句:select * from user where name = 'Arm'

由于我们给name字段添加了索引。这里的执行顺序就是:

1.先去二级索引里面找Arm,先跟根节点Lee比对,A在L的左边,找到Geek,A在G的左边。找到叶子节点Arm,该叶子节点存储的数据是Arm的主键id:10。

2.因为这里我要获取Arm的所有信息 * 。因此,二级索引无法提供完整信息,我还要根据找到的主键值10,去聚集索引里面查询。

3.重复类似过程一,从聚集索引里面找到主键id = 10的叶子节点。该叶子节点存储的数据,就是id = 10 的所有信息!

        这整个过程就是回表查询

c95294c6c39a44feb55b5f2e1375e80e.png

总结:

1.聚集索引和非聚集索引

        聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了整 行数据,有且只有一个,一般情况下主键在作为聚簇索引的。
        非聚簇索引值的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引
2.回表查询
        回表查询跟聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表。

1.5.覆盖索引


dd7a822085534226b98ec4236f760ba5.png

MySql超大分页处理问题:

504efa9ce635487fb0c6d52af49a09d1.png

解决方案:

08e1d59c524b474db405789838109a86.png

先根据id排序并从9000000条数据里面获取10条。这里根据id排序并返回id走的是覆盖索引。

再跟之前的表做关联,做一个等价查询。这样性能就有所提升了。

第一条sql是把9000010条数据全部提交给server层,然后再根据limit的第一个参数丢弃前9000000条数据,只要10条数据。

而第二条sql,是把9000010条id给server操作,相比来说就优化了。

4bafcf1fec7b4120976be330418c09f4.png

总结:

1.覆盖索引:

        覆盖索引是指select查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
        如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *,尽量在返回的列中都包含添加索引的字段。
2.MySql超大分页处理
        超大分页一般都是在数据量比较大时,我们使用了limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表中的数据就可以了因为查询id的时候,走的覆盖索引,所以效率可以提升很多。

1.6.索引创建的原则


bcabb377930d4b34882565ff6c58e706.png

c4c5fc207b3f47879e30854a0b307925.png

总结:

        一般表中的数据要超过10万以上,我们才会创建索引,并且添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件,排序字段或分组的字段这些。
        还有就是,我们通常创建索引的时候都是使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段。如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致新增改的速度变慢。
1.6.1.创建索引需要注意什么
在创建索引时,需要注意以下几点:
  1. 数据库表的大小和数据量:如果数据库表非常小或者数据量很少,可能不需要为该表创建索引。索引的主要作用是提高查询性能,如果数据量很小,查询可能已经很快了,创建索引反而会增加维护成本。
  2.  经常用于查询的字段:应该优先选择经常用于查询的字段作为索引字段。这样可以提高查询的效率,减少查询时间。
  3.  考虑查询条件的频繁性:如果某个字段在查询条件中频繁使用,那么应该考虑为该字段创建索引。这可以加快查询的速度,提高系统性能。
  4.  联合索引的使用:当查询条件中包含多个字段时,可以考虑创建联合索引。联合索引可以提高多字段查询的性能,减少索引的数量。
  5. 索引的存储空间和性能消耗:索引会占用一定的存储空间,同时也会增加写入和更新操作的性能消耗。因此,需要权衡存储空间和性能消耗,避免创建过多或不必要的索引。
  6. 定期维护索引:创建索引后,需要定期对索引进行维护,包括重建索引、优化索引等操作。这样可以保证索引的性能和稳定性。

总之,在创建索引时需要综合考虑查询需求、数据量、性能消耗等因素,合理选择索引字段和类型,以提高查询性能和系统稳定性。

1.7.索引失效的情况


b15b654e4b2842679c522e23772c5988.png

这是正常查询情况,满足最左前缀,先查有先度高的索引。

1.

c742b1cb94fb4043b781f7556db377b7.png

注意这里最后一种情况,这里和上面只查询 name = '小米科技' 的命中情况一样。说明索引部分丢失!

2.

ef2d266c71f549669181280c2765077f.png

这里第二条sql中的,status > '1'  就是范围查询

3.

3b16db3340ca4170b614f3b2aa34b8f5.png

这里sql中对name索引进行了截取,导致索引失效。

4.

4074e42f369a4935aa2da42ef7514752.png

发送类型转换,导致索引失效。

5.

b56372c619a344b8be728c6a1c063772.png

dc946e2a9e1842d0bcfef1f34c406726.png

1.8.sql的优化经验


f36154b05e254b229329d19ea8e1f36b.png

这里解释一下SQL语句的优化的原理

1.指明字段名称,可以尽量使用覆盖索引,避免回表查询,因此可以提高效率

2.字面意思,无需过多赘述。索引就是为了提高查询效率的。

3.图中两条sql直接可以使用union all 或者 union连接。使用union all 的话,就会把两条sql所有符合条件的查询结果直接合并。使用union的话,就会多一步过滤,过滤掉二者的重复项。效率相对较低。

4.避免在where子句使用对字段进行表达式操作,这也是因为可能会造成索引失效。如:1.7中索引失效的第三种情况

5.如下图,这里有两个循环。循环三次的可以看作小表,循环1000次的看作大表。

小表放外面,MySql只需要连接三次数据库,然后在里面执行1000次操作。

而把大表放外面,就相当于要连接1000次数据库!每次执行三次操作。 

而使用内连接,就会优先把小表放外面,大表放里面。而左右连接并不会改变顺序(因为编写sql的时候就已经确定了两个表的位置了)。因此优先使用内连接。

6a0e994a8e9949318bb29485e53f39c4.png

1eff076b6a42492b95371a8c7bb6f61b.png

当有写操作进来的时候,就会走Master数据库,进行写操作,然后Master数据库再把数据同步到Slave数据库。

当有读操作进来的时候,就直接走Slave进行查询。

从而实现读写分离。

01d9093529314a91bcd92f0b101a7820.png

1.9.事务


事务概念:

        事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

事务的特性:ACID:

3acd6c64a4ac45048b417981183474ea.png

小结:

ACID,分别指的是:原子性、一致性、隔离 性、持久性;

其中:原子性和一致性由undolog实现;隔离性由mvcc实现;持久性由redeolog实现

举个例子:

A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败

在转账的过程中,数据要一致,A扣除了500,B必须增加500

在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰

在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)

1.10.并发事务问题

并发事务概念:

并发事务是指在数据库系统中,多个事务同时对数据进行读写和修改的过程。

并发事务问题:

8dfe1a3ebbc44867b6d5b3a4555b00d8.png

1.脏读 (违反了事务的隔离性)

例:事务A先查询id为1的数据,再修改id为1的数据。修改完以后事务A还没提交,这时候事务B也来查询id为1的数据,但这时候事务B已经可以读到A修改完的数据了。这就是脏读。(如果A事务回滚,但B已经读到了A修改的数据,造成了数据不一致)

2.不可重复读 (违反了事务的一致性)

例:事务A先查询id为1的数据,再执行某个逻辑。这时事务B修改id为1的数据。事务A再去查询id为1的数据,却和之前不一样了!简单来说就是:在同一个事务内,查询两次同一条数据,却出现不同结果!

3.幻读 (违反了事务的隔离性)(在已经解决了不可重复读的基础上)

例:事务A查询id为1的数据,发现数据库中没有。这个时候事务B来了,正好往数据库中插入了一条id为1的数据并且提交了事务。这个时候,事务A进行插入操作,就直接报错了:里面已经有了id为1的数据(假设我们已经解决了不可重复读的问题)。但A再次进行查询,查询结果和第一次一样,发现还是没有id为1的数据,但插入就是报错,这就是幻读的问题。

注意:幻读是在解决了不可重复读的基础上,不可重复读是读取了其他事务更改的数据,针对update操作;幻读是读取了其他事务新增的数据,针对insert和delete操作。

解决方案:

6b6a89bd99614f0d8b83ab5e3115ced9.png

注:这里✔表示存在该问题,× 表示解决该问题。

小结

1.事务并发问题:

        第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改 还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数 据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依 据“脏数据”所做的操作可能是不正确的。

        第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有 结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之 间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。 这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重 复读。

         第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务 (T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在 随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就 好像发生了幻觉一样,所以称为幻读。

2.事务隔离:

        第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问 题,一般项目中也不用这个。

        第二个是读已提交(read committed)它能解 决脏读的问题的,但是解决不了不可重复读和幻读。

        第三个是可重复读 (repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个 也是mysql默认的隔离级别。

        第四个是串行化(serializable)它可以解决刚 才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以, 我们一般使用的都是mysql默认的隔离级别:可重复读。

1.11.undo log和redo log


先引入两个概念:

1b1a0667e88348d596b380fb905dc410.png

        当我们做了一些操作 (update/delete/insert),提交事务后要操作MySql中的数据。

为了能够提升性能,引入了两块区域:内存结构磁盘结构

磁盘结构

        主要存储的就是数据页,每个数据页存储的就是表中一行一行的数据。(一个表的数据,可能由多个数据页存储)。

但当我们做一些增删改的操作时,不会直接操作磁盘。而是先去操作内存

内存结构

        当操作来了以后,先来操作缓冲池。从内存中的缓冲池里面先找,有没有我要操作的数据。如果没有,就会把磁盘中的数据(某一个数据页的数据)加载到缓冲池中。这样直接操作内存,性能会更高!操作完成之后,缓冲池中数据会同步给磁盘。这样就减少磁盘的IO,加快了处理速度!

这就引出了问题:

现在内存中的数据页被操作完了,但还没同步到磁盘中 (此时这个数据页称为脏页),这个时候服务器宕机了,同步失败了。内存中的数据可能就消失了,数据丢失了。这就违背了事务持久化的特性。


解决方案:redo log (解决事务持久性)

928888e8bd36432e98155867db7980da.png

加入Redolog buffer和Redolog file后,数据操作也会发生一点变化。

         当有增删改操作之后,现在buffer pool已经发生了变化。Redolog buffer就会记录对应数据页发生的变化,一旦Redolog buffer发生变化 (说明有新的数据页变化)。就会同步给磁盘中的Redolog file中。现在一旦buffer pool对脏页数据同步失败了,就可以从Redolog file恢复数据!

注意:你可能会有疑问,这样做岂不是更麻烦了?不用这个Redolog行不行呢?比如说当buffer pool中的数据页发生变化直接进行同步不好吗?

答:这样做不好,会有严重的性能问题。当我们操作增删改的时候,会有大量的update等语句,如果我们同步刷新,对磁盘IO次数太多了,每执行一条sql可能就要进行一次连接数据库。而如果使用redolog,它进行数据同步时,都是顺序的磁盘IO (可以理解为进行了归纳,连接一次数据库会进行多条sql语句)。这样性能就可以大大提升了!

注意:

        等到脏页刷新完成后,可以认为redo log不需要了,进行定期清理,等下一个事务需要的时候,又往里面填写内容,文件不会删除,而是重复利用。两个redo log文件循环写的。


undo log:回滚日志 (解决事务一致性和原子性)

28a87cf475c5440386346470c17b7b70.png


二者区别:

   redo log 是用来记录事务对数据库进行的修改操作。如果数据库发生崩溃redo log 可以用于恢复未提交的事务对数据库所做的更改,从而确保事务的持久性
        undo log 是用来记录事务对数据库进行的修改操作的逆操作。如果一个事务需要回滚,或者数据库需要恢复到某个一致的状态,undo log 可以用于撤销已经进行的修改,从而确保事务的一致性

11a26a5cc4cc4cc68a4937892d0e9c08.png

1.12.MVCC


如何保证事务的隔离性?

1.排他锁:如一个事务获取了一个数据行的排他锁,其他事务就不能再获取改行的其他锁。

2.MVCC:多版本并发控制。

58c8c1d5224e4dc39460b7d8acbdd2b5.png

MVCC:

30c39da925854364b6c6a7110ce0493f.png

1.隐藏字段

d1562e911d084c8487736cc3614411ff.png

1.DB_TRX_ID:最近修改事务的id。默认值从0开始,每次被修改自增1。也就是说每有一个事务修改了当前数据,这条数据的该字段就会自增1。(从而实现记录最近''修改事务''的id)

2.DB_ROLL_PTR:回滚指针。举个例子:对于这里id为1的数据,进行了三次修改。DB_TRX_ID就是3。但是,.DB_ROLL_PTR记录的是该行数据第一次修改时的版本。也就是第一次修改的事务id -> 1。这样后面配合undo log就知道回滚到上一个版本(就是三次修改前的版本)。

3.DB_ROW_ID:隐藏主键。当前表指定了主键,该字段就没啥意义了。

2.undo log

37d03c9e515c437d9c4a19f4f4d999c1.png

undo log版本链

32139e37ffb347e6a8b5284f9868fe92.png

这里每有一个事务修改当前行数据,就像链表中的尾插法一样,插入一个节点。

现在有个问题就是,一个select语句过来了,我到底查询这个版本链中选择哪一条数据?而readView就是解决这个问题的!

3.readView

a4f50feb25ee406ca1e3912c882ad0ff.png

RC (读提交):

        在RC隔离级别下,事务在执行每次读取操作时都会生成一个新的读视图(Read View)。这意味着每个读取操作都会基于当前活跃的事务ID(即未提交的事务)来构建一个一致性视图(每个SELECT语句执行时都会创建一个新的Read View,以确保读取到的是最新已提交的数据)。因此,在RC隔离级别下,事务可能看到其他事务提交的更新,但这些更新必须是当前事务开始读取操作时已经存在的。换句话说,RC隔离级别允许不可重复读,即在一个事务中多次读取相同的记录可能会得到不同的结果,因为它可能看到了其他事务提交的更新。

RR (可重复读):

        相比之下,在RR隔离级别下,事务在启动时会创建一个读视图,并在整个事务的生命周期内保持不变。这意味着一旦事务开始,它将看到所有已经提交的记录的稳定视图,直到事务结束(一个事务对应一个readView)。在RR隔离级别下,即使其他事务在当前事务执行期间提交了更新,当前事务也不会看到这些变化,因为它已经有了一个稳定的数据快照。因此,RR隔离级别提供了可重复读的保证,即在一个事务中多次读取相同的记录将会得到相同的结果。

06e392b2e69144e192439beb39564d92.png

举个例子:现在是事务5执行第一条查询sql。

m_ids (当前活跃的事务):3、4、5(事务2已提交)

min_trx_id:3 (3、4、5中最小的id)

max_trx_id:6 (5+1)

creator_trx_id:5 (当前事务5做了查询,ReadView由事务5创建)

        read view中四变量。活跃事务(活跃事务就是没提交的事务)集合,当前事务id,活跃事务集合中的最小值,活跃事务集合中最大值的下个值。都是select查出的最新数据事务id跟四个变量进行比较判断的。判断方法可以逆推,就是凡是小于最小值,不包含活跃事务里面的,刚好等于当前事务的id,都可以查询最新值。其他任何情况都要一直递归查询undo日志,直到符合这三个条件为止。

版本链数据访问规则:

0f8f198e887a42629821b9529f9e40b0.png

RC级别下的访问:

3c18e6d4e87f45a0a1dcc1142dd1ae78.png

在这个例子中:根据版本链数据访问规则事务5第一个sql对应的查询结构是事务2修改后的数据。原因:根据版本链从最新事务4开始比较。

此时:creator_trx_id = 5;min_trx_id = 3;max_trx_id = 6

4所有条件都不满足,事务3也都不满足。只有事务2满足:trx_id (2) < min_trx_id(3)。

同理,对于事务5第二条查询,也能得到结果。第二条sql查询的是事务3修改后的数据。

RR级别下的访问:

dc62ddd27faf44489a488093ca4f000d.png

由于RR多次查询,都只生成最初的一个视图。因此我们只需要搞明白第一个视图的查询结果。同上方法可知,这里也是查询到事务2修改后的数据。

        在MySQL的“可重复读”(Repeatable Read)隔离级别下,生成的Read View在事务开始时创建后不会被修改。Read View是用来保证事务内部的一致性读,即在事务开始时建立的数据快照在事务的整个生命周期内是固定的。这意味着,即使事务自身对数据进行了修改,这些修改对当前事务而言是可见的但对于其他并发事务则不可见,直到当前事务提交。这样的设计确保了在同一事务中多次读取同一数据时能够得到相同的结果,从而避免了不可重复读的问题
        注意区分:同一个事务中,先select,再update,然后再select。查询结果是不一样的。因为这是同一个事务内。而如果在两次查询之间由外部事务修改数据,这时由于readView,两次查询的结果是相同的。

总结:

1077a64480b64acc9430506402922349.png

1.13.MySql主从同步原理

主从复制 (读写分离)模型:

27c6119690654afe881e687b8962833d.png

b253b1d22f5449d6b00524e999971c0a.png

小结:

34d706ccfc144f5d9859ee7c404f474e.png

1.14.分库分表

一、大致概念:

a6055cdcb336417e92e06f1c09229e0d.png

二、拆分策略:

f5a7b7c70de2446a9917652a499339ce.png

1.垂直分库:

194eecaaca244e72a0eee96022496225.png

类似微服务的拆分,根据不同业务进行拆分。

2.垂直分表:

15fe893f5416452cabf281d0c69298bc.png

这里热数据指的就是第一个表中的数据,冷数据指的就是第二个表中的‘详情数据’。

对于某些用户,我如果对当前商品不感兴趣,就只需查询大致信息,也就是第一个表中的数据。而如果我感兴趣了,就点进详情查看具体描述。这样可以减少对磁盘的IO (减少查询)。

注:这里两张表可以分库,也可以在一个数据库中分表。

3.水平分库:1c9f30344c7549ceb75fe087411537fe.png

4.水平分表:

46fcd6e50cab4c16929ab1a7b3ee72bd.png

三、分库分表所带来的问题及解决方案

9731ad2874a94bbd82f2963342975a4a.png

四、小结

e76a682931d2400ba83096c7e2ea5d58.png


二.框架篇

       1.Spring框架

1.1.Spring框架中的bean默认情况下是单例的吗?


 Spring框架中的bean默认情况下是单例的。Spring中有一个注解@Scope,这是用来设置bean的情况。默认情况下设置为singleton(单例的)

b8f67a2a6daa4e19a89c3f651336f525.png

1.2.Spring框架中的单例bean是线程安全的吗?


 9dc7080f9ee34bd0b79333304c3eee66.png

简单来说就是:判断当前成员变量能不能修改。如果是int这种可修改的类型,线程就不是安全的。

如果是,Service、DAO这些无状态类 (没有什么可变状态)就算是线程安全的。

151342b0f729447b9b2525054d073c61.png

总结:

        不是线程安全的,是这样的:
        当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
       Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
        比如:我们通常在项目中使用的Spring bean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。

1.3.什么是AOP?项目中AOP的使用?AOP的原理


AOP的概念及常见使用场景:
b79815ab59654ad580ad303221a4e28a.png
总结:
        
        aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等。
        实际项目中使用AOP的例子:
在后台管理系统中,就是使用aop来记录了系统的操作日志主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库。
SpringAOP的原理:

Spring AOP的实现原理基于动态代理和字节码操作。

在编译时,Spring会使用AspectJ编译器将切面代码编译成字节码文件。

在运行时,Spring会使用Java动态代理或CGLIB代理生成代理类,这些代理类会在目标对象方法执行前后插入切面代码,从而实现AOP的功能。

Spring AOP可以使用两种代理方式:JDK动态代理和CGLIB代理。如果目标对象实现了至少一个接口,则使用JDK动态代理;否则,使用CGLIB代理。

JDK动态代理是Java自带的动态代理实现方式。使用JDK动态代理时,需要目标对象实现至少一个接口。JDK动态代理会在运行时生成一个实现了目标对象接口的代理类,该代理类会在目标对象方法执行前后插入切面代码。

CGLIB代理是一个基于字节码操作的代理方式,它可以为没有实现接口的类创建代理对象。CGLIB代理会在运行时生成一个目标对象的子类,并覆盖其中的方法,以实现AOP的功能。

1.4.Spring中事务是如何实现的?

5c94d94e5893423784cc4ded81e32fe4.png

这里的"保存用户"方法,就是我们要加事务的方法。在该方法上添加@Transactional注解。

总结:

        声明式事务管理实际上是基于Spring的AOP(面向切面编程)机制实现的。当你在方法上使用@Transactional注解时,Spring会在运行时创建一个代理对象,这个代理对象会在方法调用前后插入事务管理的代码,从而实现事务的自动管理。

        具体来说,当Spring容器在运行时遇到带有@Transactional注解的方法时,它会创建一个代理对象来拦截这个方法。在代理对象拦截方法调用时,Spring会在方法调用前后分别插入开始事务和结束事务的代码。如果方法执行过程中抛出异常,Spring会根据异常类型决定是否回滚事务。

        因此,虽然在使用声明式事务管理时,开发者不需要自己编写AOP代码,但是Spring仍然使用了AOP技术来实现事务管理。这也是为什么有时候我们会说声明式事务管理是基于AOP的。

1.5.Spring中事务失效的场景


 d9e12dc787c94f9ba0b448a6b4191d56.png

这种情况下,自己把异常处理了,导致@Transactional没有发现异常,事务没有回滚。

02a8d6a08d1d47b785139831b06bd6f7.png

Spring默认只会回滚非检查异常也就是runtime异常。而这里的FileNotFound是一个检查异常,并不能被捕获。

d2076de84ca046369b069325d0f0bd7b.png

Spring为方法创建代理,添加事务通知,前提条件都是该方法是public修饰的!

94fff2341255418ab42bf4d03076c23a.png

1.6.Bean的生命周期


a3325816aa304555bb20091936574c34.png

1.BeanDefinition

98062c16c4f44d288a5bf503ae798996.png

2.构造函数

在此期间,调用Bean的构造函数,实例化对象 (但是还未赋值!)

3.依赖注入

Spring容器会将Bean的属性设置为Bean定义中指定的值。这个过程也被称为依赖注入,因为Spring容器会自动寻找并注入Bean所依赖的其他Bean。

4.Aware接口

用于增强Bean的功能

如果Bean实现了以Aware结尾的接口,就要重写里面的方法。图中三个接口,就是分别用于在Bean的初始化过程中,用于获取:Bean的名字、Bean的工厂、容器的上下文对象(Spring容器本身)。

5.BeanPostProcessor#before

bean的后置处理器,在初始化方法调用之前执行。

6.初始化方法

        1.InitalizingBean接口,实现了该接口,就要实现里面的方法。而在这一步就会执行重写方法。

        2.自定义init:在bean中某个方法上标注了@PostConstruct注解。就会在这一步执行这个方法。

7.BeanPostProcessor#after

bean的后置处理器,在初始化方法调用之后执行。

当一个类的功能被增强了使用到了AOP,大概率就是使用后置处理器被增强的。

  

8.销毁Bean

如果在哪个方法上标注了@PreDestroy方法,Spring容器在关闭前就会调用该方法。

注:

1.Bean的创建和初始化是分开的,第2步是创建bean,3-7是初始化赋值

2.第5步和第7步的两个后置处理器。都是某个类实现了BeanPostProcessor接口,所重写的两个方法。分别在初始化方法前后执行。

测试代码:

第一个类User类,作为我们测试的Bean

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class User implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean {

    public User() {
        System.out.println("1.User的构造方法执行了.........");
    }

    private String name;

    @Value("张三")
    public void setName(String name) {
        System.out.println("2.依赖注入执行了..........");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("3.BeanNameAware的实现方法执行了.......");
    }

    @Override
    public void setBeanName(String s) {
        System.out.println("3.BeanNameAware的实现方法执行了.......");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("3.ApplicationContextAware的实现方法执行了.......");
    }

    @PostConstruct
    public void init() {
        System.out.println("5.自定义初始化方法执行了..........");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("5.InitializingBean的实现方法执行了..........");
    }

    @PreDestroy
    public void destroy(){
        System.out.println("7.destroy方法执行了.........");
    }
}

第二个类:实现了BeanPostProcessor重写了其中方法的类。里面是两个后置处理器。

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("user")) {
            System.out.println("4.postProcessBeforeInitialization方法执行了 -> user对象初始化方法之前执行");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("user")) {
            System.out.println("6.postProcessAfterInitialization方法执行了 -> user对象初始化方法之后执行");
        }
        return 
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值