索引。

索引的原理是拿额外的存储空间换取查询时间,增加了写入数据的开销,但使读取数据的时间复杂度一般从O(n)降低到O(logn)甚至O(1)。

在数据集比较大时,不用索引就像从一本没有目录而且内容乱序的新华字典查一个字,得一页一页全翻一遍才能找到;

  • 哈希表(Hash Table):哈希表的原理可以类比银行办业务取号,给每个人一个号(计算出的Hash值),叫某个号直接对应了某个人,索引效率是最高的O(1),消耗的存储空间也相对更大。K-V存储组件以及各种编程语言提供的Map/Dict等数据结构,多数底层实现是用的哈希表。
  • 二叉搜索树(Binary Search Tree):有序存储的二叉树结构,在编程语言中广泛使用的红黑树属于二叉搜索树,确切的说是“不完全平衡的”二叉搜索树。从C++、Java的TreeSet、TreeMap,到Linux的CPU调度,都能看到红黑树的影子。Java的HashMap在发现某个Hash槽的链表长度大于8时也会将链表升级为红黑树,而相比于红黑树“更加平衡”的AVL树反而实际用的更少。
  • 平衡多路搜索树(B-Tree):这里的B指的是Balance而不是Binary,二叉树在大量数据场景会导致查找深度很深,解决办法就是变成多叉树,MongoDB的索引用的就是B-Tree。
  • 叶节点相连的平衡多路搜索树(B+ Tree):B+ Tree是B-Tree的变体,只有叶子节点存数据,叶子与相邻叶子相连,MySQL的索引用的就是B+树,Linux的一些文件系统也使用的B+树索引inode。其实B+树还有一种在枝桠上再加链表的变体:B*树,暂时没想到实际应用。
  • 日志结构合并树(LSM Tree):Log Structured Merge Tree,简单理解就是像日志一样顺序写下去,多层多块的结构,上层写满压缩合并到下层。LSM Tree其实本身是为了优化写性能牺牲读性能的数据结构,并不能算是索引,但在大数据存储和一些NoSQL数据库中用的很广泛,因此这里也列进去了。
  • 字典树(Trie Tree):又叫前缀树,从树根串到树叶就是数据本身,因此树根到枝桠就是前缀,枝桠下面的所有数据都是匹配该前缀的。这种结构能非常方便的做前缀查找或词频统计,典型的应用有:自动补全、URL路由。其变体基数树(Radix Tree)在Nginx的Geo模块处理子网掩码前缀用了;Redis的Stream、Cluster等功能的实现也用到了基数树(Redis中叫Rax)。
  • 跳表(Skip List):是一种多层结构的有序链表,插入一个值时有一定概率“晋升”到上层形成间接的索引。跳表更适合大量并发写的场景,不存在红黑树的再平衡问题,Redis强大的ZSet底层数据结构就是哈希加跳表。
  • 倒排索引(Inverted index):这样翻译不太直观,可以叫“关键词索引”,比如书籍末页列出的术语表就是倒排索引,标识出了每个术语出现在哪些页,这样我们要查某个术语在哪用的,从术语表一查,翻到所在的页数即可。倒排索引在全文索引存储中经常用到,比如ElasticSearch非常核心的机制就是倒排索引;Prometheus的时序数据库按标签查询也是在用倒排索引。

数据库主键之争:自增长 vs UUID。主键是很多数据库非常重要的索引,尤其是MySQL这样的RDBMS会经常面临这个难题:是用自增长的ID还是随机的UUID做主键?

自增长ID的性能最高,但不好做分库分表后的全局唯一ID,自增长的规律可能泄露业务信息;而UUID不具有可读性且太占存储空间。

争执的结果就是找一个兼具二者的优点的折衷方案:

用雪花算法生成分布式环境全局唯一的ID作为业务表主键,性能尚可、不那么占存储、又能保证全局单调递增,但引入了额外的复杂性,再次体现了取舍之道。


再回到数据库中的索引,建索引要注意哪些点呢?

  • 定义好主键并尽量使用主键,多数数据库中,主键是效率最高的聚簇索引;
  • 在Where或Group By、Order By、Join On条件中用到的字段也要按需建索引或联合索引,MySQL中搭配explain命令可以查询DML是否利用了索引;
  • 类似枚举值这样重复度太高的字段不适合建索引(如果有位图索引可以建),频繁更新的列不太适合建索引;
  • 单列索引可以根据实际查询的字段升级为联合索引,通过部分冗余达到索引覆盖,以避免回表的开销;
  • 尽量减少索引冗余,比如建A、B、C三个字段的联合索引,Where条件查询A、A and B、A and B and C
  • 都可以利用该联合索引,就无需再给A单独建索引了;根据数据库特有的索引特性选择适合的方案,比如像MongoDB,还可以建自动删除数据的TTL索引、不索引空值的稀疏索引、地理位置信息的Geo索引等等。

数据库之外,在代码中也能应用索引的思维,比如对于集合中大量数据的查找,使用Set、Map、Tree这样的数据结构,其实也是在用哈希索引或树状索引,比直接遍历列表或数组查找的性能高很多。

性能优化

软件设计开发某种意义上是“取”与“舍”的艺术。你无法获取无限的资源去完成无限膨胀的需求。只能在有限的条件内去完成限定范围内的事情。

在性能方面,高性能软件系统也意味着更高的实现成本,有时候与其他质量属性甚至会冲突,比如安全性、可扩展性、可观测性等等。

性能优化总结下来有两个方向:

  • 资源置换——也就是“时间”和“空间”的互换取舍。
  • 并行处理。

下面来分别说明这两个方向。


资源置换

在“时间”和“空间”的互换舍取有常见的6种方法:

  • 索引
  • 压缩
  • 缓存
  • 预取
  • 削峰填谷
  • 批量处理

索引

索引的原理是拿额外的存储空间换取查询时间,增加了写入数据的开销,但使读取数据的时间复杂度一般从O(n)降低到O(logn)甚至O(1)。

有很多种索引的类型,根据不同的场景需要使用不同的索引类型。索引还涉及到主键和分库分表的处理。

缓存

缓存优化性能的原理和索引一样,是拿额外的存储空间换取查询时间。

缓存的形式同样多种多样。从廉价的磁盘到昂贵的CPU高速缓存,最终目的都是用来换取宝贵的时间。

Phil Karlton 曾说过:There are only two hard things in Computer Science: cache invalidation and naming things.

计算机科学中只有两件困难的事情:缓存失效和命名规范。

缓存的使用除了带来额外的复杂度以外,还面临如何处理缓存失效的问题。

压缩

压缩是一个“时间换空间”的办法。

压缩的原理消耗计算的时间,换一种更紧凑的编码方式来表示数据。

为什么要拿时间换空间?时间不是最宝贵的资源吗?

举一个视频网站的例子,如果不对视频做任何压缩编码,因为带宽有限,巨大的数据量在网络传输的耗时会比编码压缩的耗时多得多。

对数据的压缩虽然消耗了时间来换取更小的空间存储,但更小的存储空间会在另一个维度带来更大的时间收益。

这个例子本质上是:“操作系统内核与网络设备处理负担 vs 压缩解压的CPU/GPU负担”的权衡和取舍。

预取

预取通常搭配缓存一起用,其原理是在缓存空间换时间基础上更进一步,再加上一次“时间换时间”,也就是:用事先预取的耗时,换取第一次加载的时间。

当可以猜测出以后的某个时间很有可能会用到某种数据时,把数据预先取到需要用的地方,能大幅度提升用户体验或服务端响应速度。

是否用预取模式就像自助餐餐厅与厨师现做的区别,在自助餐餐厅可以直接拿做好的菜品,一般餐厅需要坐下来等菜品现做。

削峰填谷

削峰填谷的原理也是“时间换时间”,谷时换峰时。

削峰填谷与预取是反过来的:预取是事先花时间做,削峰填谷是事后花时间做。就像三峡大坝可以抗住短期巨量洪水,事后雨停再慢慢开闸防水。软件世界的“削峰填谷”是类似的,只是不是用三峡大坝实现,而是用消息队列、异步化等方式。

针对不同的场景同样会使用不同的应用手段。

批量处理

批量处理同样可以看成“时间换时间”,其原理是减少了重复的事情,是一种对执行流程的压缩。以个别批量操作更长的耗时为代价,在整体上换取了更多的时间。


并行处理

并行处理也有为4种常见手段:

  • 榨干计算资源。
  • 水平扩容
  • 分片
  • 无锁

榨干计算资源

让硬件资源都在处理真正有用的逻辑计算,而不是做无关的事情或空转。

从晶体管到集成电路、驱动程序、操作系统、直到高级编程语言的层层抽象,每一层抽象带来的更强的通用性、更高的开发效率,多是以损失运行效率为代价的。

水平扩容

本节的水平扩容以及下面一节的分片,可以算整体的性能提升而不是单点的性能优化,会因为引入额外组件反而降低了处理单个请求的性能。

但当业务规模大到一定程度时,再好的单机硬件也无法承受流量的洪峰,就得水平扩容了,毕竟”众人拾柴火焰高”。

在这背后的理论基础是,硅基半导体已经接近物理极限,随着摩尔定律的减弱,阿姆达尔定律的作用显现出来。

分片

水平扩容针对无状态组件,分片针对有状态组件。二者原理都是提升并行度,但分片的难度更大。

负载均衡也不再是简单的加权轮询了,而是进化成了各个分片的协调器

无锁

有些业务场景,比如库存业务,按照正常的逻辑去实现,水平扩容带来的提升非常有限,因为需要锁住库存,扣减,再解锁库存。

票务系统也类似,为了避免超卖,需要有一把锁禁锢了横向扩展的能力。

不管是单机还是分布式微服务,锁都是制约并行度的一大因素。比如上篇提到的秒杀场景,库存就那么多,系统超卖了可能导致非常大的经济损失,但用分布式锁会导致即使服务扩容了成千上万个实例,最终无数请求仍然阻塞在分布式锁这个串行组件上了,再多水平扩展的实例也无用武之地。

避免竞争Race Condition 是最完美的解决办法。

生命在于问“为什么”?——数据库

什么是页?为什么要有页

假设没有页,mysql和磁盘间交互时,每当有⼀条数据改动,都要进⾏磁盘IO。如果修改的数据很多,那么要访问多次磁盘,性能急剧下降。
此时就会有⼀个想法“那么如果在访问磁盘时,能⼀次性修改多条数据就好了”。 所以有了页。在⼀页中可以存储多条数据。
有了页之后,mysql和磁盘间的交互是以页为单位的。⽽不是⼀条数据为单位。那么就能提升性能。

跨域请求

跨域问题是由浏览器的同源策略(Same-Origin Policy)导致的。同源策略是浏览器的一项安全策略,它限制了来自不同源的脚本在同一文档(网页)中运行,以防止恶意脚本窃取数据或进行CSRF(Cross-Site Request Forgery,跨站请求伪造)等攻击。

同源指的是协议、域名、端口号都相同的两个URL,如果两个URL中有任意一项不同,就被视为跨域。跨域请求被浏览器禁止是因为它可能会向其他域名发送敏感数据,因此浏览器默认不允许跨域请求,除非响应头中设置了允许跨域访问的策略(如CORS)或使用了跨域请求的解决方案(如JSONP)。

在实际的Web开发中,跨域问题是很常见的,比如在开发前后端分离的应用时,前端可能会向不同的服务器发起请求,这就涉及到跨域问题。因此,在开发应用时需要注意跨域问题,并采取相应的解决方案。

总结:浏览器(客户端)为了防止你向其它域名发送敏感信息,因此只允许同源(协议、域名、端口号全部相等)的请求发送。

注意:只有浏览器向服务器发送才会出现跨域问题。服务器端发起的请求并不受同源策略的限制,因此不会出现跨域问题,但可能会出现安全问题。


实现跨域

基本上有7种方法实现跨域:

JSONP(JSON with Padding):通过动态创建script,再请求一个带参网址实现跨域通信。JSONP虽然解决了跨域问题,但它存在一些缺陷,如安全性差、只支持GET请求等。随着CORS的广泛应用,JSONP的使用也逐渐被取代。

document.domain + iframe跨域:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

location.hash + iframe跨域:a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

window.name + iframe跨域:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。

postMessage跨域:可以跨域操作的window属性之一。

CORS(Cross-Origin Resource Sharing):中文名为跨域资源共享,服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求,前后端都需要设置。

代理跨域:起一个代理服务器,实现数据的转发。

Java的接口

Java一开始就有接口,但到Java8才引入了接口的默认方法。


在Java 8之前,接口只能定义抽象方法和常量。实现接口的类必须实现接口中定义的所有抽象方法。如果在接口中添加新的方法,那么所有实现该接口的类都必须修改代码以实现新的方法。

Java 8引入了接口的默认方法和静态方法,使得接口的定义更加灵活和易于使用。通过默认方法,接口可以提供一些默认的实现,而不需要强制实现这些方法。这种改进使得接口在Java中的应用更加广泛,也为Java 8中的函数式编程提供了更好的支持。


有下面这样一个叫MyInterface的接口,里面有个默认方法myDefaultMethod

interface MyInterface {
  default void myDefaultMethod() {
    System.out.println("This is a default method in MyInterface.");
  }
}

class MyClass implements MyInterface {
  // no need to implement myDefaultMethod() in MyClass
}

public class Main {
  public static void main(String[] args) {
    MyClass obj = new MyClass();
    obj.myDefaultMethod(); // output: This is a default method in MyInterface.
  }
}

继承MyInterface接口的类就不需要去实现接口中的默认方法了。在主程序中也可以直接使用默认方法。

当然,实现类也可以对默认方法进行重写。

默认方法的主要目的是为了使现有的接口能够被修改而不破坏现有的实现。


接口中的静态方法。

接口中的静态方法是指在接口中声明的带有static关键字的方法,这些方法可以通过接口名称直接调用,而无需实例化接口的实现类。

静态方法可以直接通过接口名称调用,而默认方法则必须通过实现类的实例调用。静态方法通常用于提供实用工具或服务,而默认方法用于为现有的接口提供新的功能或行为。

String、StringBuffer、StringBuilder这三者的区别。

在Java中有两种字符串的处理方式:

  • 第一种是不可变的方式:String
  • 第二种是可变的方式:StringBuffer、StringBuilder

String对象一旦被创建,就不可修改,任何的字符串操作都会返回一个新的String对象,这可能导致频繁的对象创建和销毁,影响性能。

StringBuffer和StringBuilder允许进行修改操作,提供了一种更高效的字符串处理方式。

由于String的不可变性,因此天生具备线程安全,可以在多个线程中安全使用。


而StringBuffer和StringBuilder的主要区别在于线程安全性和性能方面。

StringBuffer是线程安全的,所有方法都是同步的(底层实现加了synchronize关键字),因此可以被多个线程同时访问和修改。

StringBuilder是线程不安全的,适用于单线程环境下的字符串处理,但性能比StringBuffer更高。


因此当字符串处理不需要修改时,可以使用String;当字符串需要频繁修改时,建议使用StringBuffer或Stringbuilder


String类型是存储在字符串常量空间里的。

StringBuffer存储在堆内存里。

StringBuilder存储在堆内存里。

分布式和分布式锁

假设一个教室50个人,但只有一个厕所。

当你想上厕所的时候也有其它人想上厕所,这种情况叫冲突。

肯定不能说有内急的人一块上厕所。

如何保证在发生冲突的时候,在同一时间只有一个人可以上厕所呢?

可以制定某种规则,只有符合规则的人才能上厕所。

可以有以下规定:

  • 都想上厕所的人打一架、打赢了的人才能上厕所。
  • 剪刀石头布,赢的人上厕所。
  • 先到先得,谁先来谁上厕所。

先上厕所的人把门关上,上完厕所再把门打开。

但如果这个人一直上厕所,永远不出来也不行。因为他可能在厕所里晕倒了。因此需要在厕所设置一个闹钟,一旦超时规定时间,闹钟就会响。之后破门而入把他从厕所里面救出来。

但如果这个人确实是因为肚子不舒服需要很长的上厕所时间,但这个闹钟超过了预设的超时时间响了怎么办?强行破门后,发现对方真的是肚子不舒服,而不是晕倒在厕所。


实现分布式锁的方式

那在程序种如何实现这整套厕所的分配规则呢?

比如在数据库中有个字段为lock master,当有数据访问时设置为唯一标识,当不访问时设置为null,那其它的程序来访问时候就知道了,这个数据有人正在用。当然这个数据库可以是mysql可以是redis。

而redis的分布式也是这样的,是多个服务器去访问另一个集中的服务器,这个集中的服务器上有redis。

注意事项:用完要释放锁、锁一定要设置过期时间。(如果不加后面,如果服务器执行到一半挂了,后面的所有服务器都用不了)。如果方法执行过长,可能会释放别人的锁,可以使用续期(续期的别名叫看门狗)


实现分布式锁的工具

MySQL数据库:select for update 行级锁(最简单)(但比较吃性能),这里涉及到乐观锁和悲观锁。

redis来存储标识。(性能比较好,读写更快)支持setnx,支持lua脚本,也方便实现分布式锁

Zookeeper(不推荐,企业用的少)


实现分布式锁的实现

Redisson实现分布式锁,上面那些都不要自己写。

官网:Redisson PRO – Redis Java client with features of In-Memory Data Grid

github:redisson / redisson

给Redisson下定义(当别人问你Redisson是什么的时候,你怎么回答?):Redisson是一个用Java操作Redis的客户端。提供了大量的分布式数据集,用来简化对Redis的操作和使用。可以让开发者像使用本地集合一样使用Redis,让开发者完全感受不到Redis的存在。

补充:Redisson是Java客户端,而Java客户端就是用来操作Java的。用了Redisson可以往Redis里面增删改查。并且Redisson实现了很多Java里支持的接口和数据结构。


分布式锁的优缺点

优点:

  • 可以避免分布式系统中的资源竞争问题:在分布式系统中,多个进程或者节点可能同时对同一个资源进行访问,导致竞争和冲突。分布式锁可以通过协调进程或者节点之间的访问,避免这种竞争和冲突。
  • 提高系统可用性、降低运营和维护成本:可以不用显式配置服务器,可以部署多个服务器。
  • 可以保证数据的一致性和可靠性:分布式锁可以确保在任何时刻只有一个进程或者节点可以访问共享资源,从而保证数据的一致性和可靠性。

缺点:

  • 增加开发成本,增加系统复杂度:使用分布式锁需要引入一些新的组件或者服务,比如 ZooKeeper 等,这会增加系统的复杂度。
  • 会增加系统的延迟:由于需要通过网络进行通信,分布式锁会增加系统的延迟。在高并发的情况下,这个延迟可能会很大。
  • 可能会出现死锁:由于分布式锁涉及多个节点或者进程之间的协调,如果协调不当,就可能会出现死锁问题。

synchronized关键字。

为什么需要synchronized关键字?

在多线程环境下,如果多个线程同时访问一个共享资源,就会出现多个线程同时修改这个资源的情况,从而导致数据不一致等问题。

线程同步指的是多个线程在访问共享资源时保持数据一致性的过程。

并发安全指的是在多线程并发访问下,程序能够正确、稳定地运行,并且不会出现数据竞争、数据不一致等问题。

但无论是线程同步并发安全、还是线程安全等类似的词语,都是为了在多线程环境下保持数据的一致性,并保证程序按照预期的结果执行。


synchronized如何解决这个情况?

synchronized是一个用于解决并发安全的修饰关键字。它保证了多线程环境下数据操作的原子性

具体来说。使用synchronized可以保证同一时刻只有一个线程访问该资源,其他线程需要等待当前线程执行完毕后才能访问,从而避免了线程不安全的问题。

synchronized可以修饰在方法或代码块上。方法细分的话可以分为普通方法和静态方法。

  • 修饰普通方法:锁的对象是调用这个方法的对象。一个对象用一把锁。
  • 修饰静态方法:锁对象是当前类对象。类的所有对象公用一把锁。
  • 修饰代码块:锁对象是括号里指定的对象。尽量不要使用 synchronized(String a) 。

尽量不要使用synchronized(String a)的原因是,String对象是不可变的(immutable),也就是说,一旦创建了一个String对象,其值就不会再发生改变。而在Java中,字符串常量(如”abc”)是被共享的,也就是说,多个变量可以引用同一个字符串常量。

当使用synchronized关键字来同步访问一个字符串常量时,实际上是在同步访问该字符串常量所对应的字符串池(string pool)中的对象。由于字符串常量是不可变的,因此在字符串池中的字符串对象可能会被其他代码(如String.intern()方法)引用,从而导致其他代码也受到了同步访问的影响,这可能会导致意想不到的问题。

因此,尽量不要使用synchronized(String a)来同步访问字符串常量,而应该使用其他对象来作为锁,例如自定义的对象或Class对象。


如何使用synchronized?

修饰方法:在方法声明中添加synchronized关键字,使得整个方法在执行时都会获取对象的锁。

public class Counter {
    private int count;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }
}

如果一个对象有多个synchronize方法,一个线程访问了这个对象的一个synchronize方法,其他线程就不能访问这个对象的任何一个synchronize方法。

多个不同实例对象的 synchronize 方法之间没有关系

修饰静态方法:下面是一个使用synchronized修饰静态方法的例子:

public class Counter {
    private static int count;
    
    public static synchronized void increment() {
        count++;
    }

    public static synchronized void decrement() {
        count--;
    }

    public static synchronized int getCount() {
        return count;
    }
}

修饰代码块:使用synchronized关键字修饰一个代码块,可以使用以下方式:

public class Counter {
    private int count;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public void decrement() {
        synchronized (lock) {
            count--;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

synchronized做了什么?

synchronized保证了原子性可见性有序性,即保证在同一个锁上,一个线程修改了共享变量的值之后,另一个线程能够立即看到修改后的值,并且在多个线程执行顺序上保证了一致性。

防止指令重排序,保证代码执行的顺序和预期一致。

实现线程间的通信,虽然synchronized没法直接作用于线程通信,但可以实现线程间的协调和同步,从而达到线程间的通信的效果。

例如,在生产者-消费者模型中,生产者线程负责生产数据,消费者线程负责消费数据,它们需要进行协调和同步。可以使用synchronized关键字来保证生产者和消费者线程在对共享数据进行读写时的互斥,从而防止出现数据竞争的问题。另外,可以使用wait()notify()notifyAll()等方法来实现线程间的等待、通知和唤醒操作,从而实现线程间的协调和同步。

synchronized会带来什么?

一种技术的引用会带来好处也会带来弊端。

 synchronized会带来一定的性能损失,因为每次进入同步块时都需要获得锁,这会增加线程的等待时间和上下文切换的开销。

同时,如果同步块的代码执行时间很短,也会增加不必要的性能开销。因此,需要根据具体情况来判断是否需要使用synchronized


synchronized还有哪些问题?

synchronized和其他同步工具(如 ReentrantLock)相比,有以下优缺点:

  • 优点:简单易用,不需要手动释放锁。
  • 缺点:不灵活,不能设置超时时间、中断等;效率低,每次都要进入内核态获取锁;不可重入,同一个线程在获取到锁后还要再次获取锁会造成死锁。

有哪些注解可以注入 Bean?@Autowired 和 @Resource 的区别?


有哪些注解可以注入 Bean?

  • @Autowired:由Spring提供的注解。自动注入,按照类型自动装配,如果有多个同类型的Bean,则需要通过@Qualifier指定具体的Bean。
  • @Resource:由JDK提供的注解,在Java 5中被引入。按照名称自动装配,默认是按照属性名称进行匹配,如果需要按照 Bean 的名称进行匹配,可以使用@Resource(name=”beanName”)。
  • @Inject:由JSR-330提供,Java SE 6引入。和@Autowired类似,也是按照类型进行自动装配。
  • @Value:由Spring提供的注解。用于注入配置文件中的属性值,可以指定默认值。
  • @Component:由Spring提供的注解。用于声明一个Bean,作用类似于XML中的<bean>标签。

@Autowired 和 @Resource 的区别?

@Autowired按照类型自动装配更为常用,而@Resource按照名称自动装配则比较适合需要明确指定Bean名称的情况。

public class UserService {

    // @Autowired 注解
    @Autowired
    private UserDao userDao;

    // @Resource 注解
    @Resource(name = "userDao")
    private UserDao userDao;
}

另外需要注意的是,@Autowired 注解默认情况下需要依赖对象存在,如果依赖对象为 null,则会抛出异常。而 @Resource 注解默认情况下不需要依赖对象存在,即依赖可以为 null,但可以通过 required 属性进行控制。


@Autowired

在 Spring 中,@Autowired 注解用于自动装配 bean。其中,自动装配的类型是根据被注入的目标类型(Target Type)来确定的,也就是说,Spring 会查找和目标类型相同或者子类型的 bean 进行自动装配。具体来说,如果被注入的目标类型是一个接口或者一个抽象类,则 Spring 会查找实现该接口或抽象类的所有 bean 进行自动装配。如果目标类型是一个具体的类,则 Spring 只会查找该类型对应的 bean 进行自动装配。

public interface UserDao {
    // ...
}

@Service
public class UserDaoImpl implements UserDao {
    // ...
}

@Service
public class UserService {

    private final UserDao userDao;

    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    // ...
}

在上述代码中,UserService 类中的 userDao 字段使用了 @Autowired 注解进行了自动装配。由于 userDao 的类型是 UserDao 接口,因此 Spring 会查找所有实现了 UserDao 接口的 bean 进行自动装配。在这个例子中,UserDaoImpl 类实现了 UserDao 接口,因此 Spring 会将 UserDaoImpl 实例注入到 UserService 中。

注意,如果存在多个实现了 UserDao 接口的 bean,而没有明确指定要注入哪一个 bean,则 Spring 会抛出异常。在这种情况下,我们可以使用 @Qualifier 注解或者在 bean 定义时使用 @Primary 注解来指定要注入的 bean。


@Resource

@Resource 注解中,name 属性指定了需要注入的 bean 的名称,而不是指定需要注入的类名。具体来说,如果 name 属性未指定,则默认采用字段名或者属性名作为 bean 的名称进行注入,例如:

public class UserService {

    @Resource
    private UserDao userDao;

    @Resource(name = "myUserDao")
    private UserDao userDao;

}

在上述代码中,@Resource 注解未指定 name 属性,因此默认使用了 userDao 作为需要注入的 bean 的名称。因此,Spring 将会查找名称为 userDao 的 bean 进行注入。

name 属性指定了 myUserDao,因此 Spring 将会查找名称为 myUserDao 的 bean 进行注入。注意,这里的名称应该是对应 bean 的名称,而不是对应类的名称。在 Spring 中,我们通常可以通过在 bean 的定义中使用 id 或者 name 属性来指定 bean 的名称。

MySQL支持几种存储引擎

show ENGINES

使用上面的SQL脚本可以查询出MySQL支持的存储引擎。

我这里的版本是MySQL的8.0.31版本。

MySQL-8.0.31下存储引擎

可以得到大致这样的表。

整体来说常用有以下7(6+1)种吧:

  • InnoDB:MySQL的默认存储引擎,支持事务、支持行级锁、支持表级锁、支持外键约束。不支持全文索引、不支持压缩表。
  • MyISAM:MySQL 最早提供的存储引擎,不支持事务、不支持行级锁机制,也不支持外键约束。进支持表级锁、支持全文索引、支持压缩表。
  • MEMORY:这种类型的数据表只存在于内存中,使用散列索引,数据的存取速度非常快。因为是存在于内存中,所以这种类型常应用于临时表中。
  • ARCHIVE:英文意为档案,用于只是偶尔需要查询的历史数据进行存储,将数据进行压缩存储,占用空间小,但不支持索引和更新操作。
  • Merge:MySQL 5.5版本后,Merge存储引擎已经被弃用并从MySQL中删除。因此在上面的表中看不到。将多个相同的 MyISAM 表合并为一个虚表,常应用于日志和数据仓库。
  • CSV:将数据以CSV格式存储,适合用于导入和导出数据。
  • BLACKHOLE:英文意为黑洞。这种存储引擎不实际存储数据,像黑洞一样,所有写入的数据都会被丢弃,但可以记录数据的写入日志。

InnoDB和MyISAM的相同点和不同点。

相同点:

InnoDB 和 MyISAM 实现索引都是使用 B+ 树,只是实现方式不同。

不同点:

事务行级锁表级锁外键约束全文索引压缩表高并发
InnoDB
MyISAM 

这里解释以下,InnoDB是支持高并发的,MyISAM 也有一定支持,但性能非常差劲。读操作和写操作是相互阻塞的,也就是说,在一个写操作完成之前,所有的读操作都必须等待。这意味着在高并发环境下,MyISAM可能会面临严重的性能问题。


存储引擎的选择。

在选择存储引擎时,需要根据应用程序的需求和特点进行选择。如果需要支持事务和外键约束,以及并发性能比较重要,应该选择 InnoDB。大多数情况下你都可以选择InnoDB。

如果需要读取数据的性能比较重要,可以选择 MyISAM。如果需要全文搜索索引或表压缩,应该选择 MyISAM。

通常认为,MyISAM在读取数据方面的性能表现较好,在大量读取的情况下效率更高。而 InnoDB 在处理事务和大量并发查询的情况下性能更好。选择存储引擎的时候需要根据应用程序的读写比例和并发性能的需求来选择。但也要时刻记住下面这段话。

MySQL 高性能》:

不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。