Heim >Java >javaLernprogramm >Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

Java后端技术全栈
Java后端技术全栈nach vorne
2023-08-23 14:54:26721Durchsuche

Der Hauptinhalt dieses Artikels lautet wie folgt:

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

1. Das Problem lokaler Sperren

Betrachten wir zunächst das Problem lokaler Sperren:

Derzeit ist der Microservice in Die Frage ist in vier Microservices unterteilt. Wenn Front-End-Anfragen eingehen, werden sie an verschiedene Microservices weitergeleitet. Wenn das Front-End 10-W-Anfragen empfängt und jeder Mikrodienst 2,5-W-Anfragen empfängt und der Cache ausfällt, wird jeder Mikrodienst beim Zugriff auf die Datenbank durch die Sperre (synchronzied oder lock) zum Sperren eigene Thread-Ressourcen, um zu verhindern, dass Cache-Aufschlüsselung. synchronziedlock)来锁住自己的线程资源,从而防止缓存击穿

这是一种本地加锁的方式,在分布式Dies ist ein lokale Sperre, in Distributed führt zu Dateninkonsistenzproblemen: Nachdem Dienst A beispielsweise die Daten erhalten hat, aktualisiert er den Cache-Schlüssel =100, Dienst B jedoch nicht durch die Sperre von Dienst A eingeschränkt und aktualisiert gleichzeitig den Cache-Schlüssel = 99. Das Endergebnis kann 99 oder 100 sein, dies ist jedoch ein unbekannter Zustand und stimmt nicht mit dem erwarteten Ergebnis überein. Das Flussdiagramm sieht wie folgt aus:

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

2. Was ist eine verteilte Sperre

Basierend auf dem oben genannten Problem der lokalen Sperre benötigen wir eine Sperre, die die verteilte Clusterumgebung unterstützt: Beim Abfragen der Datenbank kann nur ein Thread darauf zugreifen und andere Threads Sie können darauf zugreifen. Sie müssen warten, bis der erste Thread die Sperrressource freigibt, bevor Sie mit der Ausführung fortfahren.

Fall aus dem Leben: Sie können sich das Schloss als ein Schloss außerhalb der Tür vorstellen,所有并发线程比作 Sie alle wollen den Raum betreten, und nur eine Person kann den Raum betreten. Wenn jemand hereinkommt, schließen Sie die Tür ab und andere müssen warten, bis die Person, die eingetreten ist, herauskommt.

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

Werfen wir einen Blick auf die Grundprinzipien verteilter Sperren, wie in der Abbildung unten dargestellt:

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

Lassen Sie uns die verteilten Sperren im Bild oben analysieren:

  • 1. Das Front-End leitet 10-W-Anfragen mit hoher Parallelität an vier Themen-Microservices weiter.
  • 2. Jeder Microservice verarbeitet 2,5 W-Anfragen.
  • 3. Jeder Thread, der eine Anfrage verarbeitet, muss die Sperre ergreifen, bevor er das Geschäft ausführen kann. Es kann als „Besetzen einer Grube“ verstanden werden.
  • 4. Der Thread, der die Sperre erworben hat, gibt die Sperre nach Abschluss des Geschäfts frei. Es kann als „Freilassen der Grube“ verstanden werden.
  • 5. Der Thread, der nicht erfasst wurde, muss warten, bis die Sperre aufgehoben wird.
  • 6. Nachdem die Sperre aufgehoben wurde, ergreifen andere Threads die Sperre.
  • 7. Wiederholen Sie die Schritte 4, 5 und 6.

Umgangssprachliche Erklärung: Alle angeforderten Threads gehen an denselben Ort"Occupy the pit" Wenn eine Grube vorhanden ist, wird die Geschäftslogik ausgeführt. Wenn keine Grube vorhanden ist, müssen andere Threads die „Grube“ freigeben. Diese Grube ist für alle Threads sichtbar. Sie können diese Grube in den Redis-Cache oder die Redis-Datenbank einfügen : „Operator Mono“, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(155, 110, 35);background-color: rgb(255, 245, 227);padding: 3px; margin: 3px;">"Distributed pit". “占坑”,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放“坑位”。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库,这篇讲的就是如何用 Redis 做“分布式坑位”

三、Redis 的 SETNX

Redis 作为一个公共可访问的地方,正好可以作为“占坑”的地方。

用 Redis 实现分布式锁的几种方案,我们都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况。

我们来看下这个命令,SETNXset If not exist的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。

在 Redis 命令行中是这样执行的:

set <key> <value> NX

我们可以进到 redis 容器中来试下 SETNX

3. SETNX von Redis

Redis kann als öffentlich zugänglicher Ort als Ort zum „Ausnutzen“ genutzt werden.

Mehrere Lösungen zum Implementieren verteilter Sperren mit Redis. Wir alle verwenden den SETNX-Befehl (den Schlüssel auf einen bestimmten Wert setzen). Lediglich die Anzahl der im High-Level-Schema übergebenen Parameter ist unterschiedlich und es werden abnormale Situationen berücksichtigt. 🎜🎜Schauen wir uns diesen Befehl an: SETNX ist Kurzform für set If not exist. Dies bedeutet, dass, wenn der Schlüssel nicht vorhanden ist, der Wert des Schlüssels festgelegt wird und nichts unternommen wird, wenn er vorhanden ist. 🎜🎜So wird es in der Redis-Befehlszeile ausgeführt: 🎜
docker exec -it <容器 id> redis-cli
🎜Wir können in den Redis-Container gehen und es ausprobieren SETNX Befehl. 🎜🎜Betreten Sie zuerst den Container: 🎜
docker exec -it <容器 id> redis-cli

然后执行 SETNX 命令:将 wukong 这个 key 对应的 value 设置成 1111

set wukong 1111 NX

返回 OK,表示设置成功。重复执行该命令,返回 nil表示设置失败。

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

四、青铜方案

我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。

3.1 青铜原理

我们来看下流程图:

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant
  • 多个并发线程都去 Redis 中申请锁,也就是执行 setnx 命令,假设线程 A 执行成功,说明当前线程 A 获得了。
  • 其他线程执行 setnx 命令都会是失败的,所以需要等待线程 A 释放锁。
  • 线程 A 执行完自己的业务后,删除锁。
  • 其他线程继续抢占锁,也就是执行 setnx 命令。因为线程 A 已经删除了锁,所以又有其他线程可以抢占到锁了。

代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent

setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
  // 2.抢占成功,执行业务
  List<TypeEntity> typeEntityListFromDb = getDataFromDB();
  // 3.解锁
  redisTemplate.delete("lock");
  return typeEntityListFromDb;
} else {
  // 4.休眠一段时间
  sleep(100);
  // 5.抢占失败,等待锁释放
  return getTypeEntityListByRedisDistributedLock();
}

一个小问题:那为什么需要休眠一段时间?

因为该程序存在递归调用,可能会导致栈空间溢出。

3.2 Nachteile der Bronzelösung

Bronze wird Bronze genannt, weil sie am elementarsten ist und definitiv viele Probleme verursachen wird.

Stellen Sie sich eine Familienszene vor: Nachts schließt Xiao Kong alleine die Tür auf und betritt den Raum, macht das Licht an? Und dann plötzlich Der Strom ist aus. Xiao Kong möchte die Tür öffnen und hinausgehen, kann aber die Position des Türschlosses nicht finden. Dann kann Xiao Ming nicht hineinkommen und auch nicht irgendjemand draußen. 断电了,小空想开门出去,但是找不到门锁位置,那小明就进不去了,外面的人也进不来。

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

从技术的角度看:setnx 占锁成功,业务代码出现异常或者服务器宕机,没有执行删除锁的逻辑,就造成了死锁

那如何规避这个风险呢?

设置锁的自动过期时间,过一段时间后,自动删除锁,这样其他线程就能获取到锁了。

四、白银方案

4.1 生活中的例子

上面提到的青铜方案会有死锁问题,那我们就用上面的规避风险的方案来设计下,也就是我们的白银方案。

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

还是生活中的例子:小空开锁成功后,给这款智能锁设置了一个沙漏倒计时⏳

Aus technischer Sicht: setnx hat die Sperre erfolgreich besetzt, wann Der Geschäftscode ist abnormal oder der Server ist ausgefallen. Die Logik zum Löschen der Sperre wird nicht ausgeführt, was zu deadlock .

Wie kann man dieses Risiko vermeiden?

Sperre festlegenAutomatische Ablaufzeit, nach einer gewissen Zeit wird die Sperre automatisch gelöscht , damit andere Threads die Sperre erhalten können. 🎜

4. Silberplan 🎜

🎜🎜4.1 Beispiele aus dem Leben🎜🎜🎜Der oben erwähnte Bronzeplan wird Deadlock-Probleme haben, daher werden wir den oben genannten Risikovermeidungsplan verwenden, um ihn zu entwerfen, der unser ist Silberplan. 🎜
🎜Ein Beispiel aus dem Leben: nachdem Xiao Kong die Tür erfolgreich aufgeschlossen hatte , legen Sie eine Sanduhr-Countdown⏳, nachdem die Sanduhr abgelaufen ist, wird die Tür verriegelt automatisch öffnen. Selbst wenn es im Raum zu einem plötzlichen Stromausfall kommt, öffnet sich das Schloss nach einer Weile automatisch und andere können hineinkommen. 🎜🎜🎜4.2 Technisches Schema 🎜🎜🎜Der Unterschied zur Bronzelösung besteht darin, dass nach erfolgreicher Belegung der Sperre die Ablaufzeit der Sperre Schritt für Schritt festgelegt wird. Wie im Bild unten gezeigt: 🎜
Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

4.3 示例代码

清理 redis key 的代码如下

// 在 10s 以后,自动清理 lock
redisTemplate.expire("lock", 10, TimeUnit.SECONDS);

完整代码如下:

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
    // 2.在 10s 以后,自动清理 lock
    redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.解锁
    redisTemplate.delete("lock");
    return typeEntityListFromDb;
}

4.4 白银方案的缺陷

白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:

因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。

所以和青铜方案有一样的问题:锁永远不能过期

五、黄金方案

5.1 原子指令

上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。

原子性:多条命令要么都成功执行,要么都不执行。

将两步放在一步中执行:占锁+设置锁过期时间。

Redis 正好支持这种操作:

# 设置某个 key 的值并设置多少毫秒或秒 过期。
set <key> <value> PX <多少毫秒> NX
或
set <key> <value> EX <多少秒> NX

然后可以通过如下命令查看 key 的变化

ttl <key>

下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。

# 设置 key=wukong,value=1111,过期时间=5000ms
set wukong 1111 PX 5000 NX
# 查看 key 的状态
ttl wukong

执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

5.2 技术原理图

黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

5.3 示例代码

设置 lock 的值等于 123,过期时间为 10 秒。如果 10 秒 以后,lock 还存在,则清理 lock。

setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);

5.4 黄金方案的缺陷

我们还是举生活中的例子来看下黄金方案的缺陷。

5.4.1 Benutzer A hebt die Sperre auf

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant
  • Benutzer A hat die Sperre zuerst aufgehoben und die Sperre so eingestellt, dass sie nach 10 Sekunden automatisch entsperrt wird. Die Sperrnummer ist 123. 123
  • 10 秒以后,A 还在执行任务,此时锁被自动打开了。

5.4.2 用户 B 抢占锁

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant
  • 用户 B 看到房间的锁打开了,于是抢占到了锁,设置锁的编号为 123,并设置了过期时间 10 秒
  • 因房间内只允许一个用户执行任务,所以用户 A 和 用户 B 执行任务产生了冲突
  • 用户 A 在 15 s 后,完成了任务,此时 用户 B 还在执行任务。
  • 用户 A 主动打开了编号为 123
  • 10 Sekunden später führt A immer noch die Aufgabe aus und das Schloss wird automatisch geöffnet. 5.4.2 Benutzer B übernimmt die Sperre
  • Benutzer B sah, dass das Schloss des Raums offen war, also ergriff er das Schloss und stellte die Schlossnummer auf 123 und legen Sie die Ablaufzeit fest10 Sekunden.
  • Da nur ein Benutzer Aufgaben im Raum ausführen darf, führen Benutzer A und Benutzer B Aufgaben aus Produzierter Konflikt.

Benutzer A in 15 s später ist die Aufgabe abgeschlossen und Benutzer B ist fertig immer noch auf einer Mission.

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant
Benutzer A ergriff die Initiative und öffnete den Code mit der Nummer 123 lock.
  • 🎜Benutzer B führt die Aufgabe immer noch aus und stellt fest, dass das Schloss geöffnet wurde. 🎜🎜🎜🎜Benutzer B war sehr wütend: 🎜Ich habe die Aufgabe noch nicht abgeschlossen, warum hat sich das Schloss geöffnet? 🎜🎜🎜🎜🎜5.4.3 Benutzer C greift auf das Schloss🎜🎜🎜🎜🎜🎜🎜Nachdem das Schloss von Benutzer B aktiv von A geöffnet wurde, verlässt A den Raum, während B noch Aufgaben erledigt.
  • Benutzer C übernimmt die Sperre und C beginnt mit der Ausführung der Aufgabe.
  • Da nur ein Benutzer Aufgaben im Raum ausführen darf, besteht ein Konflikt zwischen den von Benutzer B und Benutzer C ausgeführten Aufgaben.

Aus dem obigen Fall können wir das erkennen, da die Zeit, die Benutzer A zum Verarbeiten der Aufgabe benötigt größer ist als die Zeit, die für die automatische Reinigung (Entsperrung) der Sperre benötigt wird. Nachdem die Sperre also automatisch entsperrt wurde, Andere Benutzer haben die Sperre aufgehoben. Wenn Benutzer A die Aufgabe abschließt, öffnet er aktiv die von anderen Benutzern beschlagnahmten Schlösser.

Warum werden hier die Schlösser anderer Leute geöffnet? Da alle Schlossnummern aufgerufen werden “123”,用户 A 只认锁编号,看见编号为 “123”, wird das Schloss von Benutzer B geöffnet. Zu diesem Zeitpunkt hat Benutzer B die Aufgabe noch nicht abgeschlossen, daher ist er natürlich verärgert. 6. Platinum-Plan Das Bild unten zeigt, dass die von B vorbelegte Sperre blau ist, was sich von der von A vorbelegten grünen Sperre unterscheidet. Auf diese Weise wird es nicht von A geöffnet.

Eine Animation erstellt, um das Verständnis zu erleichtern:

Animationsdemonstration

Das statische Bild ist hochauflösender, Sie können einen Blick darauf werfen:

6.2 Der Unterschied zwischen dem technischen Schaltplan

und die goldene Lösung :Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant
  • 设置锁的过期时间时,还需要设置唯一编号。
  • 主动删除锁的时候,需要判断锁的编号是否和设置的一致,如果一致,则认为是自己设置的锁,可以进行主动删除。
Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

6.3 代码示例

// 1.生成唯一 id
String uuid = UUID.randomUUID().toString();
// 2. 抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(lock) {
    System.out.println("抢占成功:" + uuid);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.获取当前锁的值
    String lockValue = redisTemplate.opsForValue().get("lock");
    // 5.如果锁的值和设置的值相等,则清理自己的锁
    if(uuid.equals(lockValue)) {
        System.out.println("清理锁:" + lockValue);
        redisTemplate.delete("lock");
    }
    return typeEntityListFromDb;
} else {
    System.out.println("抢占失败,等待锁释放");
    // 4.休眠一段时间
    sleep(100);
    // 5.抢占失败,等待锁释放
    return getTypeEntityListByRedisDistributedLock();
}
  • 1.生成随机唯一 id,给锁加上唯一值。
  • 2.抢占锁,并设置过期时间为 10 s,且锁具有随机唯一 id。
  • 3.抢占成功,执行业务。
  • 4.执行完业务后,获取当前锁的值。
  • 5.如果锁的值和设置的值相等,则清理自己的锁。

6.4 铂金方案的缺陷

上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant
  • 时刻:0s。线程 A 抢占到了锁。

  • 时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。

  • 时刻:10s。锁自动过期。

  • 时刻:11s。线程 B 抢占到锁。

  • 时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。

  • 时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。

那如何规避这个风险呢?钻石方案登场。

七、钻石方案

上面的线程 A 查询锁和删除锁的逻辑不是原子性的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。

7.1 技术原理图

如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。

Redis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant

7.2 代码示例

那如何用脚本进行删除呢?

我们先来看一下这段 Redis 专属脚本:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。

那么这段脚本怎么在 Java 项目中执行呢?

分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。

// 脚本解锁
String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

上面的代码中,KEYS[1] 对应“lock”,ARGV[1] 对应 “uuid”,含义就是如果 lock 的 value 等于 uuid 则删除 lock。

而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。

那钻石方案是不是就完美了呢?有没有更好的方案呢?

下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。

8. Zusammenfassung

Dieser Artikel erweitert das Problem verteilter Sperren durch das Problem lokaler Sperren. Anschließend werden fünf verteilte Sperrlösungen vorgestellt und die Verbesserungen verschiedener Lösungen von flach bis tief erläutert.

Durch die kontinuierliche Weiterentwicklung der oben genannten Lösungen wissen wir, wo im System ungewöhnliche Situationen auftreten können und wie wir besser damit umgehen können.

In Analogie kann dieses sich weiterentwickelnde Denkmodell auch auf andere Technologien angewendet werden.

Im Folgenden werden die Mängel und Verbesserungen der oben genannten fünf Lösungen zusammengefasst.

Bronze-Lösung:

  • Fehler: Der Geschäftscode ist abnormal oder der Server ist ausgefallen und die Logik zum aktiven Löschen der Sperre wird nicht ausgeführt, was zu einem Deadlock führt.
  • Verbesserung: Legen Sie die automatische Ablaufzeit der Sperre fest. Nach einer bestimmten Zeit wird die Sperre automatisch gelöscht, sodass andere Threads die Sperre erhalten können.

Silberlösung:

  • Defekt: Das Besetzen der Sperre und das Festlegen der Ablaufzeit der Sperre erfolgen in zwei Schritten und sind keine atomaren Vorgänge.
  • Verbesserung: Das Besetzen von Sperren und das Festlegen der Ablaufzeit der Sperren stellen atomare Operationen sicher.

Goldener Plan:

  • Fehler: Wenn die Sperre aktiv gelöscht wird, werden die von anderen Clients belegten Sperren gelöscht, da die Sperrwerte gleich sind.
  • Verbesserung: Jedes Mal, wenn die Sperre belegt ist, wird sie zufällig auf einen größeren Wert gesetzt. Wenn die Sperre aktiv gelöscht wird, wird der Wert der Sperre mit dem von Ihnen festgelegten Wert verglichen, um festzustellen, ob er gleich ist.

Platin-Lösung:

  • Fehler: Diese drei Schritte des Erwerbs der Sperre, des Vergleichs des Sperrwerts und des Löschens der Sperre sind nicht atomar. Es ist möglich, dass die Sperre auf halbem Weg automatisch abgelaufen ist und von anderen Clients übernommen wurde, sodass die von anderen Clients belegten Sperren beim Löschen der Sperre gelöscht wurden.
  • Verbesserung: Verwenden Sie Lua-Skripte, um atomare Vorgänge zum Erwerb von Sperren, zum Vergleichen von Sperren und zum Löschen von Sperren durchzuführen.

Diamantplan:

  • Mängel: Unprofessionelle Lösung für verteilte Schlösser.
  • Verbesserung: Redission verteilte Sperre.

The King Plan, wir sehen uns im nächsten Artikel~

Das obige ist der detaillierte Inhalt vonRedis verteilte Sperre|Fünf Evolutionspläne von Bronze zu Diamant. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:Java后端技术全栈. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen