0%

Bug

1. panic: close of closed channel

在server中,只使用了一个全局的管道来接收命令应用结果,PutAppend和Get共享一个管道,两个分别打开管道,随后一个关闭,另一个在关闭时出现问题

  • 给两个操作加锁,只有操作执行完(成功执行,超时)才解锁
  • 对Start返回的index,每一个添加一个管道

2. command被复制到半数以上服务器,但是还没有在状态机上执行,然后选举出新的leader,新的leader具有添加的命令

在Lab2就想到的问题,一直考虑已经被添加的command如何被再次添加,发现在raft层无法解决这个问题。

在Lab3中遇到了此问题,考虑在server中记录client已经添加的命令(命令可能未执行成功),在收到更小的command时,则不调用Start添加到leader。这需要启用新的server作为leader时,快速将已有的命令执行完毕(在添加任何新命令之前),这一步难以实现。

因此考虑在command中添加client的命令标志,重传的命令也可以被添加到log中,但是在执行时会发现该命令已经执行过

命令没有被执行 TestManyPartitionsManyClients3A

=== RUN   TestManyPartitionsManyClients3A
Test: partitions, many clients (3A) ...

命令结果有额外部分(多执行了?)TestConcurrent3A

Test: unreliable net, restarts, partitions, random keys, many clients (3A) ...
info: wrote history visualization to /tmp/1021001197.html
test_test.go:382: history is not linearizable
--- FAIL: TestPersistPartitionUnreliableLinearizable3A (30.13s)

3. 并发读写map

fatal error: concurrent map read and map write

replyCh := make(chan ApplierResult, 1)
kv.appliedCh[index] = replyCh
//kv.appliedCh[index] = make(chan ApplierResult, 1)

CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。

1 CAP理论

CAP 也就是 Consistency(一致性)Availability(可用性)Partition Tolerance(分区容错性)

  • 一致性(Consistency) : 所有节点访问同一份最新的数据副本
  • 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
  • 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。

Quorum机制(法定人数机制
分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。

2 数据一致性

一些分布式系统通过复制数据来提高系统的可靠性和容错性,并且将数据的不同的副本存放在不同的机器,由于维护数据副本的一致性代价高,因此许多系统采用弱一致性来提高性能,一些不同的一致性模型也相继被提出。

  • 强一致性: 要求无论更新操作实在哪一个副本执行,之后所有的读操作都要能获得最新的数据。

  • 弱一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。

  • 最终一致性:是弱一致性的一种特例,保证用户**最终(即窗口尽量长)**能够读取到某操作对系统特定数据的更新。

[!question] 分布式一致性(缓存与数据库一致性)

  1. 分布式事务:两段提交
  2. 分布式锁
  3. 消息队列、消息持久化、重试、幂等操作
  4. Raft / Paxos 等一致性算法

3 BASE理论

BASE 是 Basically Available(基本可用)Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。

3.1 基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。

什么叫允许损失部分可用性呢?

  • 响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。

  • 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。

3.2 软状态

软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

3.3 最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

分布式一致性的 3 种级别:

  1. 强一致性:系统写入了什么,读出来的就是什么。
  2. 弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
  3. 最终一致性:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。

业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。

4 共识算法

共识算法的作用是让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。提案的含义在分布式系统中十分宽泛,像哪一个节点是 Leader 节点、多个事件发生的顺序等等都可以是一个提案。

共识是可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。

共识算法允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,其正确性主要是源于复制状态机的性质:一组Server的状态机计算相同状态的副本,即使有一部分的Server宕机了它们仍然能够继续运行。
image.png

https://javaguide.cn/distributed-system/protocol/raft-algorithm.html

5 Basic Paxos 算法

  • 提议者(Proposer):也可以叫做协调者(coordinator),提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。

  • 接受者(Acceptor):也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史;

  • 学习者(Learner):如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。
    image.png|450

6 Raft

6.1 基础

6.1.1 节点类型

  • Leader:负责发起心跳,响应客户端,创建日志,同步日志。

  • Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。

  • Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。

6.1.2 任期

6.1.3 日志

6.2 领导人选举

raft 使用心跳机制来触发 Leader 的选举。

如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。

Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫做选举超时,然后它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。

6.3 日志复制

一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(Replicated State Machine)执行的命令。

Leader 收到客户端请求后,会生成一个 entry,包含<index,term,cmd>,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。

如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。

如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以称这个 entry 是 committed 的,并且向客户端返回执行结果。

raft 保证以下两个性质:

  • 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd

  • 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同

7 主从、集群、分布式的区别

  • 分布式:多个系统协同合作完成一个特定任务的系统。分布式是解决中心化管理的问题,把所有的任务叠加到一个节点处理,太慢了。所以把一个大的问题拆分为多个小的问题,并分别解决,最终协同合作。分布式的主要工作是分解任务,将职能拆解。

  • 集群:集群主要的使用场景是为了分担请求的压力,也就是在几个服务器上部署相同的应用程序,来分担客户端请求。当压力进一步增大的时候,可能在需要存储的部分,mysql 无法面对很多的写压力。因为在 mysql 做成集群之后,主要的写压力还是在 master 的机器上面,其他 slave 机器无法分担写压力,从而这个时候,也就引出来分布式。

将一套系统拆分成不同子系统部署在不同服务器上(这叫分布式),

然后部署多个相同的子系统在不同的服务器上(这叫集群),部署在不同服务器上的同一个子系统应做负载均衡。

分布式:一个业务拆分为多个子业务,部署在多个服务器上 。

集群:同一个业务,部署在多个服务器上 。

主从、集群和分布式是计算机系统中常见的架构模式,它们有不同的特点和用途:

  1. 主从(Master-Slave):

    • 主从架构是一种单点控制的架构,其中有一个主节点和一个或多个从节点。
    • 主节点通常负责处理所有的请求和决策,而从节点用于执行主节点分派的任务或保存数据的备份副本。
    • 主从架构通常用于提高系统的可用性和容错性。如果主节点失败,可以将其中一个从节点提升为主节点,以保持系统的运行。
    • 主从架构适用于那些需要单一决策权和数据同步的应用,如数据库复制、负载均衡等。
  2. 集群(Cluster):

    • 集群是由多个节点组成的计算机系统,这些节点共同协作以提供某种服务或功能。
    • 集群节点通常是对等的,它们可以相互协作,共同处理请求,以提高性能和容错性。
    • 集群可以用于各种用途,包括负载均衡、高可用性、并行计算等。
    • 集群可以是对称的(每个节点都具有相同的角色和功能)或非对称的(某些节点具有特殊的角色,如主节点)。
  3. 分布式(Distributed):

    • 分布式架构是指系统的组件分布在多个地理位置或计算节点上,它们通过网络通信协同工作。
    • 分布式系统的目标是提高性能、扩展性和可用性,允许系统在多个节点上并行执行任务。
    • 分布式系统可以包括多个集群,每个集群可能都有自己的主从结构,以满足系统的需求。
    • 分布式系统通常需要处理分布式计算、数据同步、一致性和容错性等复杂问题。

总之,主从是一种单点控制的架构,集群是多个节点共同协作的架构,分布式是多个节点分布在不同地方并通过网络通信协同工作的架构。这些不同的架构模式在不同的应用场景中有不同的优点和局限性。选择哪种架构取决于应用的需求和目标。

集群是个物理形态,分布式是个工作方式。分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提升效率。
分布式:一个业务分拆多个子业务,部署在不同的服务器上。
集群:同一个业务,部署在多个服务器上

term(任期)

Leader发出heartbeat(AppendEntries RPC不带有log entries)

Raft is a consensus algorithm that is designed to be easy to understand. It’s equivalent to Paxos in fault-tolerance and performance.

QQ图片20240205215524.jpg

1 2A Leader election(领导人选举)

  • 网络延迟、分区、包丢失、复制和重新排序。

This election term will continue until a follower stops receiving heartbeats and becomes a candidate.

导致Follower进行选举的原因

  • 网络延迟或者包丢失没有在选举超时前收到心跳

  • 网络分区而导致收不到心跳(disconnect)

  • Leader宕机、崩溃(crash)

1.1 节点类型

  • Leader:负责发起心跳,响应客户端,创建日志,同步日志。

  • Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。

  • Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。态转换

image-20240203110252535

  • Follower$\rightarrow$Candidate(超时:一段时间内未收到heartbeat)

Leader(AppendEntries RPC)

  • term过时:

    • 发送心跳发现Server的term号大于自身的term号(已经选出新的Leader,将自己状态变为Follower)
    • 收到RequestVote发现更高term号

Candidate(RequestVote RPC)

  • term过时

    • 收到新的Leader的心跳,请求投票时发现大于自身的term号
    • 收到RequestVote RPC的response,返回的term号更大

Follower:被动的,对来自Leader和Candidate的请求进行相应

  • 收到Leader的心跳,且term号大于自身term号(更新自身term号)

  • 收到Candidate请求

Server:

  • 拒绝过时term的请求

1.2 选举超时时间和心跳时间

  • 心跳间隔时间(heartbeat timeout):Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。

  • 选举超时时间(election timeout):如果一个 Follower 在一个周期内没有收到心跳信息或者请求投票信息,就叫做选举超时,并开始一次新的选举。

这两个时间需要保持一定的关系,网络无故障时,在选举超时前应该收到心跳,以保持Leader不变。选举超时时间至少需要大于AppendEntries RPC发送到server所需的最长时间。

1.2.1 选举超时时间更新

  • candidate成为leader

  • candidate收到RequestVote response并变为follower

  • server收到AppendEntries

  • server收到RequestVote

  • leader收到AppendEntries response发现更高term号

1.2.2 超时选举实现

在raft结构体中定义laskAcktime,在收到leader的heartbeat或者candidate的投票请求时,要更新选举超时时间这里没有通过定时器在到达选举超时时间后触发选举操作,而是首先记录下当前时间,然后让ticker协程sleep一段时间。当再次唤醒后,如果laskAcktime在startTime之后,说明在选举超时前收到了相关信号。
image.png

1.3 注意点

1.3.1 sendAppendEntries没有启用新的协程

sendHeartbeat中,异步发送sendAppendEntries,向所有server发送heartbeat,无需等待RPC完成。因为leader可能无法与其他server通信,或者server不可达,由于等待rpc返回,造成超时重新选举

异步发送RequestVote,并且收到超过半数选票后就成为Leader,发送心跳

go test -race -run 2A

2 2B Log replication(日志复制)

Leader Completeness: if a log entry is committed in a given term, then that entry will be present in the logs of the leaders for all higher-numbered terms. §5.4
State Machine Safety: if a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for the same index. §5.4.3

  • leader收到client请求,将entry添加到log

  • leader将添加的entry复制到其他server

  • 如果大多数server成功复制entry,则该entry已经committed

  • 已经committed的entry需要应用到机器上,lastApplied代表已经执行的命令

Raft-第 1 页.png

2.1 Election restriction(5.4.1)

Raft-第 2 页.png

log backtracking

Raft-加速log回溯

S1首先成为leader,成功发送了第一个entry

S1和S2与其他服务器断开后重连

2.2 注意点

遇到的bug

s1断联,添加了一系列entry

重连后,新的leader通过heartbeat更新了s1的commitindex,而此时s1的log还未更新

3 2C

persist
// Your code here (2C).
// Example:
// w := new(bytes.Buffer)
// e := labgob.NewEncoder(w)
// e.Encode(rf.xxx)
// e.Encode(rf.yyy)
// data := w.Bytes()
// rf.persister.SaveRaftState(data)
readPersist
// Your code here (2C).
// Example:
// r := bytes.NewBuffer(data)
// d := labgob.NewDecoder(r)
// var xxx
// var yyy
// if d.Decode(&xxx) != nil ||
// d.Decode(&yyy) != nil {
// error...
// } else {
// rf.xxx = xxx
// rf.yyy = yyy
// }

3.1 Bug

同一任期选出两个leader

乱序收到RPC response

AppendEntries的RPC response与当前term不一致

在变成follower时都重置了election time

4 2D

image-20240215120732611

4.1 bug

  1. 每10条command创建一个快照,快照会调用rf.mu这个互斥锁,当存在新的提交命令时,通过applyCh管道进行发送,而测试程序调用snapshot需要获取互斥锁,无法读取管道中的数据,则会产生死锁

  2. apply error: server 2 apply out of order, expected index 10, got 18

    当snapshot存在未commit的命令时,snapshot和log分别放入applyCh,应当一次性放入applyCh

result the time that the test took in seconds the number of Raft peers the number of RPCs sent during the test the total number of bytes in the RPC messages the number of log entries that Raft reports were committed
PASSED 3.9 3 490 154736 207

5 问题

5.1 raft

raft是分布式一致性算法,基于复制状态机的思想,对于初始状态一样的节点,在他们上运行同样的命令,还会保持一致性的状态。raft主要包括领导者选举、日志复制、持久化以及快照等。

5.2 raft应用场景

Raft 是一种共识算法,通常用于构建分布式系统中的可靠复制日志。它可以应用于各种分布式系统的场景,包括但不限于:

  1. 分布式数据库系统:Raft 可以用于构建分布式数据库系统,确保数据的一致性和可靠性,比如 etcd、Consul 等。

  2. 分布式文件系统:在分布式文件系统中,Raft 可以确保各个节点之间的文件操作的一致性,比如 HDFS、Ceph 等。

  3. 分布式消息队列:Raft 可以确保消息队列中的消息传递和处理的一致性,比如 Kafka 等。

  4. 分布式计算:在分布式计算中,Raft 可以确保各个节点之间的任务调度和执行的一致性,比如 Spark、MapReduce 等。

  5. 分布式存储系统:Raft 可以确保分布式存储系统中数据的可靠性和一致性,比如分布式缓存系统如 Redis、分布式块存储系统如 Ceph 等。

总的来说,任何需要在分布式环境中保证一致性和可靠性的系统都可以考虑使用 Raft 算法来实现。

6 参考

Raft Consensus Algorithm 官网介绍

Raft (thesecretlivesofdata.com)(Raft图示)

Raft作者博士论文

Instructors’ Guide to Raft[4]

Students’ Guide to Raft[5]

Raft Q&A[6]

paxos 2015版的lab

hashicorp/raft: Golang implementation of the Raft consensus protocol (github.com)

如何的才能更好地学习 MIT6.824 分布式系统课程? - 知乎 (zhihu.com)

rpc: gob error encoding body: gob: type mr.JobConfirm has no exported fields

rpc.go中的定义struct中变量要大写

interface类型转换

value    interface{}
op,ok:= value.(要转换的类型)
value.(type)//获取类型

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

数组类型 [n]T 表示拥有 n 个 T 类型的值的数组。

var a [10]int

primes := [6]int{2, 3, 5, 7, 11, 13}

var s []int = primes[1:4]

range

//当使用 `for` 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

映射
make 函数会返回给定类型的映射,并将其初始化备用。

var m map[string]Vertex
m = make(map[string]Vertex)
elem, ok := m[key]
delete(m, key)
elem = m[key]

哈希表

Insert or update an element in map m:

m[key] = elem

Retrieve an element:

elem = m[key]

Delete an element:

delete(m, key)

Test that a key is present with a two-value assignment:

elem, ok = m[key]

If key is in m, ok is true. If not, ok is false.

If key is not in the map, then elem is the zero value for the map’s element type.

Note: If elem or ok have not yet been declared you could use a short declaration form:

elem, ok := m[key]

go panic: assignment to entry in nil map(没有初始化)

并发

var wg sync.WaitGroup
wg.add(1)
go func(){
wg.Done()
}()
wg.Wait()
var wg sync.WaitGroup
for i:=0;i<5;i++{
wg.Add(1)
go func(x int){
sendRPC(x)
wg.Done()
}(i);
wg.Wait()
}


var wg sync.WaitGroup
for i:=0;i<5;i++{
wg.Add(1)
go func(){
sendRPC
()
wg.Done()
}();
wg.Wait()
}

45. 跳跃游戏 II

0位置可以跳到的区间为[p,q],在到达q时,从0位置一定跳到[p,q]中某一位置从[p,q]中某一位置可以到达的最远位置为maxPos
从绿色区间、蓝色区间分别进行一次跳跃

image.png

class Solution {
public:
int jump(vector<int>& nums) {
int step=0;
int maxPos=0; //当前可以达到的最远位置
int limit=0;

for(int i=0;i<nums.size()-1;i++){
maxPos=max(maxPos,i+nums[i]);
if(i==limit){
step++;
limit=maxPos;
}
}
return step;
}
};

1 循环依赖方式

1.1 构造参数循环依赖

public class A{
private B b;
public void A(B b){
this.b=b;
}
}

public class B{
private A a;
public void B(A a){
this.a=a;
}
}
<bean id="a" class="...A">
<constructor-arg index="0" ref="b"></constructor-arg>
</bean>

<bean id="b" class="...B">
<constructor-arg index="0" ref="a"></constructor-arg>
</bean>

Spring先创建(singleton) A,A依赖B,将A放进当前创建Bean池中;然后创建B,B依赖A,将B放进当前创建Bean池中,然后发现A已经在池中。

1.2 setter方式(单例,singleton),默认方式

image.png

Spring是先将Bean对象实例化之后再设置对象属性的

1.3 setter方式(原型,prototype)

scope=“prototype”,每次请求都会创建一个实例对象

2 循环依赖(circular reference)

在 Spring 中,循环依赖是指两个或多个 Bean 之间互相依赖的情况,导致 Bean 的创建发生循环,从而无法完成依赖注入的过程。这种情况可能会导致应用程序启动失败或者出现不可预测的行为。

Spring 框架提供了一些解决循环依赖的机制:

  1. 提前曝光(Early Exposure): Spring 容器在创建 Bean 的过程中,会提前暴露正在创建的 Bean 实例,以解决循环依赖的问题。当一个 Bean A 依赖另一个 Bean B,而 Bean B 又依赖 Bean A 时,Spring 在创建 Bean A 的过程中,会提前暴露一个代理对象,用于处理 Bean B 对 Bean A 的依赖。这样,Bean A 可以在被完全创建之前,通过代理对象来访问 Bean B。这种方式需要使用 CGLIB 来创建代理对象。

  2. 构造函数注入: 使用构造函数注入来解决循环依赖问题。Spring 容器在创建 Bean 的过程中,会首先将依赖项通过构造函数传递进去,从而避免了循环依赖的问题。这种方式需要谨慎使用,因为构造函数注入会将循环依赖暴露在类的构造函数中,可能导致代码不够清晰。

  3. Setter 方法注入: 使用 Setter 方法注入来解决循环依赖问题。与构造函数注入类似,通过将依赖项通过 Setter 方法注入,可以避免循环依赖的问题。与构造函数注入相比,Setter 方法注入更加灵活,可以在 Bean 创建完成后再进行依赖注入,但也需要注意循环依赖可能带来的问题。

虽然 Spring 提供了这些解决循环依赖的机制,但是在设计应用程序时,尽量避免出现循环依赖是更好的选择。循环依赖会导致代码的复杂性增加,降低程序的可维护性和可读性。

3 三级缓存解决循环依赖

  • 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。

  • 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。

  • 三级缓存(singletonFactories):存放ObjectFactoryObjectFactorygetObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

3.1 Spring 创建 Bean 的流程:

  • 先去 一级缓存 singletonObjects 中获取,存在就返回;

  • 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;

  • 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。

[!note]
只用两级缓存够吗? 在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。

4 Spring源码

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

/** Maximum number of suppressed exceptions to preserve. */
private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;


/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从 singletonObjects 获取实例,singletonObjects 中缓存的实例都是完全实例化好的 bean,可以直接使用
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
synchronized(this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 加入到三级缓存,暴漏早期对象用于解决循环依赖
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}

return singletonObject != NULL_OBJECT ? singletonObject : null;
}


}
  • singletonObjects:用于存放初始化好的 bean 实例,存放完整对象。

  • earlySingletonObjects,用于存放初始化中的 bean,来解决循环依赖。存放半成品对象,属性还未赋值的对象。

  • singletonFactories:用于存放 bean 工厂,bean 工厂所生成的 bean 还没有完成初始化 bean。存放的是 ObjectFactory<?> 类型的 lambda 表达式,就是这用于处理 AOP 循环依赖的。

相互引用的bean,A依赖B,把A原始对象包装成SingletonFactory 放入三级缓存
B依赖A,B依赖的A是从singletonFactories获取bean工厂调用getObject方法生产bean放入earlySingletonObjects。

5 参考

  1. 面试必问:Spring 循环依赖的三种方式 - 知乎 (zhihu.com)

  2. 面经手册 · 第31篇《Spring Bean IOC、AOP 循环依赖解读》 | 小傅哥 bugstack 虫洞栈

  3. Spring 循环依赖那些事儿(含Spring详细流程图) - 知乎 (zhihu.com)

  4. 什么是循环依赖以及解决方式-CSDN博客

  5. Spring Bean 循环依赖 - spring 中文网 (springdoc.cn)(*****)

  6. Spring源码最难问题《当Spring AOP遇上循环依赖》_循环依赖aop在那个阶段-CSDN博客

  7. Spring 中的控制反转(IoC)和依赖注入(DI) - spring 中文网 (springdoc.cn)

[!note] 核心特性

  • IoC容器:Spring通过控制反转实现了对象的创建和对象间的依赖关系管理。开发者只需要定义好Bean及其依赖关系,Spring容器负责创建和组装这些对象。
  • AOP:面向切面编程,允许开发者定义横切关注点,例如事务管理、安全控制等,独立于业务逻辑的代码。通过AOP,可以将这些关注点模块化,提高代码的可维护性和可重用性。
  • 事务管理:Spring提供了一致的事务管理接口,支持声明式和编程式事务。开发者可以轻松地进行事务管理,而无需关心具体的事务API。
  • MVC框架:Spring MVC是一个基于Servlet API构建的Web框架,采用了模型-视图-控制器(MVC)架构。它支持灵活的URL到页面控制器的映射,以及多种视图技术。

1 Spring、Spring MVC、Spring Boot

Spring 包含了多个功能模块,其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。

Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。

使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!

Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。

1.1 Spring MVC工作原理

  • DispatcherServlet核心的中央处理器,负责接收请求、分发,并给予客户端响应。

  • HandlerMapping处理器映射器,根据 URL 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。

  • HandlerAdapter处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler

  • Handler请求处理器,处理实际请求的处理器。

  • ViewResolver视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端

image.png

流程说明(重要):

  1. 客户端(浏览器)发送请求, DispatcherServlet拦截请求。

  2. DispatcherServlet 根据请求信息调用 HandlerMappingHandlerMapping 根据 URL 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。

  3. DispatcherServlet 调用 HandlerAdapter适配器执行 Handler

  4. Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServletModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View

  5. ViewResolver 会根据逻辑 View 查找实际的 View

  6. DispaterServlet 把返回的 Model 传给 View(视图渲染)。

  7. View 返回给请求者(浏览器)

2 Spring Bean

Bean 代指的就是那些被 IoC 容器所管理的对象。

2.1 Bean作用域

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。

  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。

  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。

  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。

  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。

  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。

@Bean @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Person personPrototype() { return new Person(); }

2.2 Bean是否线程安全

prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。

大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量。

  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

2.3 Bean生命周期

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。

  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value 注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。

  3. Bean 初始化

    • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
    • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
    • 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
    • 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
    • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
    • 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。
    • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
    • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法。
  4. 销毁 Bean:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源

    • 如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
    • 如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy 注解标记 Bean 销毁之前执行的方法。

[!Note]

  • BeanNameAware:注入当前 bean 对应 beanName;
  • BeanClassLoaderAware:注入加载当前 bean 的 ClassLoader;
  • BeanFactoryAware:注入当前 BeanFactory 容器的引用。
  • BeanPostProcessor 接口是 Spring 为修改 Bean 提供的强大扩展点
    • postProcessBeforeInitialization:Bean 实例化、属性注入完成后,InitializingBean#afterPropertiesSet方法以及自定义的 init-method 方法之前执行;
    • postProcessAfterInitialization:类似于上面,不过是在 InitializingBean#afterPropertiesSet方法以及自定义的 init-method 方法之后执行。

image.png

  • 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。

  • 初始化这一步涉及到的步骤比较多,包含 Aware 接口的依赖注入、BeanPostProcessor 在初始化前后的处理以及 InitializingBeaninit-method 的初始化操作。

  • 销毁这一步会注册相关销毁回调接口,最后通过DisposableBeandestory-method 进行销毁。

image.png
Spring常见面试题总结 | JavaGuide

3 注解

3.1 将一个类声明为Bean的注解

import org.springframework.stereotype.Component;
@Component
  • @Component:标注一个类为Spring容器的Bean,把普通pojo实例化到spring容器中,相当于配置文件中的<bean id=“” class=“”/>

  • @Repository:对应持久层即Dao层,主要用于数据库相关操作

  • @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。

  • @Controller:对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。表示Web层实现。

3.1.1 @Component 和 @Bean 的区别是什么?

  • @Component 注解作用于类,而@Bean注解作用于方法。

  • @Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。

3.2 注入Bean的注解

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource@Inject 都可以用于注入 Bean。

Annotation Package Source
@Autowired org.springframework.bean.factory Spring 2.5+
@Resource javax.annotation Java JSR-250
@Inject javax.inject Java JSR-330

3.2.1 @Autowired 和 @Resource 的区别是什么?

Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

[!note]
这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

//`SmsService` 接口有两个实现类: `SmsServiceImpl1`和 `SmsServiceImpl2`,且它们都已经被 Spring 容器所管理。

// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;

@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType

如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName

// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;

[!tip]

  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显式指定名称,@Resource可以通过 name 属性来显式指定名称。
  • @Autowired 支持在构造函数、方法、字段和参数上使用。@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。

image.png

4 事务

在 Spring 框架中, @Transactional注解是用来开启事务的,但它的工作原理是通过代理对象来实现的。 当你在一个 public方法上加上 @Transactional注解时,Spring 会生成一个代理对象,该代理对象负责管理事务。 但是,对于 private方法,由于其访问权限的限制,Spring 无法生成代理对象,因此事务也无法正常生效

4.1 事务隔离级别

  • TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别

  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

  • TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

  • TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

  • TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

5 BeanFactory

生产 bean 的工厂,它负责生产和管理各个 bean 实例。

image.png

  • ListableBeanFactory:通过这个接口,我们可以获取多个 Bean

  • HierarchicalBeanFactory:在应用中起多个 BeanFactory,然后可以将各个 BeanFactory 设置为父子关系。

  • AutowireCapableBeanFactory:自动装配Bean

  • ConfigurableListableBeanFactory

0.1 反射机制

应用场景:动态代理、注解

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

获取Class对象

一个类的对象表示,一个Class对象表示一个特定类的属性

//TargteObject表示一个类
1. 知道具体类
Class c=TargetObject.class;
2. 通过 Class.forName()传入类的全路径获取:
Class c=Class.forName("cn.javaguide.TargetObject");
3. 通过对象实例获取
TargetObject o = new TargetObject();
Class c = o.getClass();
4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
  • c.getDeclaredConstructors()

    • getParameterTypes()
  • c.getDeclaredMethods()

  • c.getDeclaredFields()

类名.方法名
方法名.invoke(类名)

1 注解

Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解本质是一个继承了Annotation 的特殊接口:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

public interface Override extends Annotation{

}

JDK 提供了很多内置的注解(比如 @Override@Deprecated),同时,我们还可以自定义注解。

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。

  • 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value@Component)都是通过反射来进行处理的。

2 代理

2.1 动态代理(proxy)

为指定的接口在系统运行期间动态地生成代理对象动态代理机制的实现主要由一个类和一个接口组成, 即java. lang. reflect . Proxy类和java .lang. reflect.InvocationH.andler接口

image.png

public interface Animal {  
void info();
}

public class Dog implements Animal{
@Override
public void info() {
System.out.println("Animal Dog");
}
}

public class AnimalInvocationHandler implements InvocationHandler {
private Object target;
public AnimalInvocationHandler(Object target){
this.target=target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("info")){
return method.invoke(target,args);
}
}
}

public class AgentTest {
@Test
public void test_proxy(){
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

Animal animal=(Animal) Proxy.newProxyInstance(classLoader,new Class[]{Animal.class},new AnimalInvocationHandler(new Dog()));
animal.info();
}
}

当Proxy动态生成的代理对象上相应的接口方法被调用时,对应的Invocat ionHandler就会拦截相应的方法调用,并进行相应处理。InvocationHandler就是我们实现横切逻辑的地方,它是横切逻辑的载体,作用跟Advice是一样的。

2.2 CGLIB(动态字节码生成, Code Generation Library)

动态字节码生成技术扩展对象行为:对目标对象进行继承扩展,为其生成相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系统使用扩展后的目标对象的子类,就可以达到与代理模式相同的效果。
CGLIB可以对实现了某种接口的类,或者没有实现任何接口的类进行扩

public class Requestable {  
public void request(){
System.out.println("In class Requestable, method request");
}
}

public class RequestCallback implements MethodInterceptor{
public Object newInstall(Object object) {
return Enhancer.create(object.getClass(), this);
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if(method.getName().equals("request")){
System.out.println("Before request");
return methodProxy.invokeSuper(o,objects);
}
return null;
}
}

public class AgentTest {
@Test
public void test_cglib(){
//通过CGLIB的Enhancer为目标对象动态地生成一个子类
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(Requestable.class);
enhancer.setCallback(new RequestCallback());
//通过为enhancer指定需要生成的子类对应的父类,以及Callback实现
//enhancer最终为我们生成了需要的代理对象实例。
Requestable proxy=(Requestable) enhancer.create();
proxy.request();

RequestCallback requestCallback=new RequestCallback();
Requestable requestable=(Requestable) requestCallback.newInstall(new Requestable());
requestable.request();
}
}

3 序列化

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

简单来说:

  • 序列化:将数据结构或对象转换成二进制字节流的过程

  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。

下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;

  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;

  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;

  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

4 Spring

4.1 代理Bean注册到Spring容器

类的调用是不能直接调用没有实现的接口的,所以需要通过代理的方式给接口生成对应的实现类。接下来再通过把代理类放到 Spring 的 FactoryBean 的实现中,最后再把这个 FactoryBean 实现类注册到 Spring 容器。那么现在你的代理类就已经被注册到 Spring 容器了,接下来就可以通过注解的方式注入到属性中。

面经手册 · 第28篇《你说,怎么把Bean塞到Spring容器?》 | 小傅哥 bugstack 虫洞栈

spring-framework\spring-beans\src\main\java\org\springframework\beans\factory\support\InstantiationStrategy:实例化策略抽象接口
spring-framework\spring-beans\src\main\java\org\springframework\beans\factory\support\SimpleInstantiationStrategy.java:简单的对象实例化功能

5 参考

Java 反射机制详解 | JavaGuide(Java面试+学习指南)

1 细粒度锁

  • 最小化锁范围: 细粒度锁将锁的作用范围限制在最小的数据单元或操作上,而不是对整个数据结构或资源加锁。
  • 减少锁持有时间: 通过快速完成操作并尽早释放锁,减少每个线程持有锁的时间,从而减少其他线程的等待时间。
  • 锁分离:将不同的操作或数据访问分离到不同的锁上,使得多个操作可以并行执行,而不是所有操作都依赖于同一个锁。
  • 锁分段:将数据结构分割成多个段,每个段有自己的锁,这样可以同时对不同段进行操作而不会相互阻塞。
  • 无锁编程: 利用原子操作和数据结构来避免使用锁,通过CAS(Compare-And-Swap)等机制来保证数据的一致性。
  • 读写锁:允许多个读操作同时进行,但写操作需要独占锁,以此来提高读操作的并发性。
  • 锁粗化: 在某些情况下,如果一个线程需要连续访问多个资源,可以考虑将锁的范围扩大,以减少频繁的锁获取和释放。
  • 锁升级和降级:根据实际情况动态调整锁的粒度,例如从读锁升级到写锁,或者在不同级别的锁之间进行切换。
  • 避免死锁: 设计锁策略时,确保锁的获取和释放顺序一致,避免出现死锁。
  • 性能监控: 监控锁的性能,包括锁的竞争率、等待时间和吞吐量,以便根据实际情况调整锁策略。

1.1 条件变量

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.ReentrantLock;

import java.util.List;

import java.util.ArrayList;


public class TaskScheduler {

private final List<Runnable> tasks = new ArrayList<>();

private final ReentrantLock lock = new ReentrantLock();

private final Condition condition = lock.newCondition();


public void addTask(Runnable task) {

lock.lock();

try {

tasks.add(task);

condition.signalAll();

} finally {

lock.unlock();

}

}


public void executeTasks() {

lock.lock();

try {

while (tasks.isEmpty()) {

condition.await();

}

Runnable task = tasks.remove(0);

task.run();

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

} finally {

lock.unlock();

}

}

}

2 高并发架构

2.1 负载均衡

2.2 分库分表

通过分库分表,可以极大的减少单点数据库的压力,提高查询效率。通过分库分表,主要解决数据量大、并发访问高的问题。通过将同一表的数据分布到多个数据库实例中,从而,提升系统的水平扩展能力。

跨多个数据库实例的事务操作变得复杂,可能需要分布式事务管理

2.3 消息队列

消息队列在系统中引入异步消息处理机制,可以削峰填谷,缓解瞬时高并发压力。

2.4 异步处理

异步处理通过将一些非实时任务放入队列异步执行,减少实时系统的响应时间。

2.5 缓存

分布式缓存,通过将频繁访问的数据,存储在快速访问的介质中,减少数据库、和后端服务的压力。

2.6 服务拆分

将大型单体应用拆分为多个小型服务(微服务架构),各服务可以独立扩展和部署。

  • 按业务功能划分

  • 按边界上下文划分

  • 数据库分离

  • 限流和熔断:限流用于控制系统的流量,使系统不至于被过载流量压垮,保护系统在高并发下的稳定性,防止系统崩溃。熔断器用于在下游服务不稳定时,快速返回错误以保护系统。

3 参考

  1. 高并发设计之细粒度锁 : 5种细粒度锁的设计技巧图解(高并发篇) (qq.com)

  2. 高并发架构方案详解(8主流高并发架构) (qq.com)

  3. 高并发解决方案详解(8大主流架构方案) (qq.com)