


Wie das Saga-Muster verteilte Transaktionsprobleme löst: Methoden und Beispiele aus der Praxis
1. Das Problem verstehen: Die Komplexität verteilter Transaktionen
Verteilte Transaktionen umfassen mehrere Mikrodienste, wobei jeder Dienst einen Teil einer Transaktion ausführt. Beispielsweise könnte eine E-Commerce-Plattform Dienste wie Zahlung, Inventar und Auftragsverwaltung umfassen. Diese Dienste müssen zusammenarbeiten, um eine Transaktion abzuschließen. Was passiert jedoch, wenn einer dieser Dienste ausfällt?
1.1 Ein reales Szenario
Stellen Sie sich eine E-Commerce-Anwendung vor, bei der während einer Bestellung die folgenden Schritte ausgeführt werden:
- Schritt 1: Zahlung vom Konto des Kunden abbuchen.
- Schritt 2: Reduzieren Sie die Anzahl der Artikel im Inventar.
- Schritt 3: Erstellen Sie eine Bestellung im Auftragsverwaltungssystem.
Wenn der Inventardienst nach dem Abzug der Zahlung, aber vor der Erstellung der Bestellung fehlschlägt, befindet sich das System in einem inkonsistenten Zustand. Dem Kunden wird eine Gebühr berechnet, es wird jedoch keine Bestellung aufgegeben.
1.2 Traditionelle Lösungen und ihre Grenzen
Um solche Fehler zu bewältigen, könnte man die Verwendung einer verteilten Transaktion mit einem zweiphasigen Commit-Protokoll in Betracht ziehen. Dies führt jedoch zu mehreren Problemen:
- Hohe Latenz: Jeder Dienst muss während der Transaktion Ressourcen sperren, was zu einer erhöhten Latenz führt.
- Reduzierte Verfügbarkeit: Wenn ein Dienst ausfällt, wird die gesamte Transaktion zurückgesetzt, wodurch die Gesamtsystemverfügbarkeit verringert wird.
- Enge Kopplung: Dienste werden eng gekoppelt, wodurch es schwieriger wird, einzelne Dienste zu skalieren oder zu ändern.
2. Wie das Saga-Muster das Problem löst
In verteilten Systemen erstrecken sich Transaktionen oft über mehrere Microservices. Es ist eine Herausforderung sicherzustellen, dass alle Dienste entweder erfolgreich oder gar nicht abgeschlossen werden. Die herkömmliche Vorgehensweise – die Verwendung verteilter Transaktionen mit zweiphasigem Commit – kann aufgrund von Problemen wie hoher Latenz, enger Kopplung und verringerter Verfügbarkeit problematisch sein.
Das Saga-Muster bietet einen flexibleren Ansatz. Anstatt zu versuchen, eine Transaktion als einzelne Einheit auszuführen, zerlegt das Saga-Muster die Transaktion in kleinere, isolierte Schritte, die unabhängig voneinander ausgeführt werden können. Jeder Schritt ist eine lokale Transaktion, die die Datenbank aktualisiert und dann den nächsten Schritt auslöst. Wenn ein Schritt fehlschlägt, führt das System Ausgleichsmaßnahmen durch, um die in den vorherigen Schritten vorgenommenen Änderungen rückgängig zu machen und sicherzustellen, dass das System zu einem konsistenten Zustand zurückkehren kann.
2.1 Was ist das Saga-Muster?
Das Saga-Muster ist im Wesentlichen eine Folge kleinerer Transaktionen, die nacheinander ausgeführt werden. So funktioniert es:
- Lokale Transaktionen: Jeder an der Transaktion beteiligte Dienst führt seine eigene lokale Transaktion durch. In einem Auftragsabwicklungssystem könnte beispielsweise ein Dienst die Zahlung, ein anderer den Lagerbestand und ein anderer den Bestelldatensatz übernehmen.
- Ereignis- oder Nachrichtenveröffentlichung: Nachdem ein Dienst seine lokale Transaktion abgeschlossen hat, veröffentlicht er ein Ereignis oder sendet eine Nachricht, die den erfolgreichen Abschluss dieses Schritts anzeigt. Beispielsweise könnte der Zahlungsdienst nach der Verarbeitung der Zahlung ein „PaymentCompleted“-Ereignis veröffentlichen.
- Auslösen des nächsten Schritts: Der nächste Dienst in der Sequenz wartet auf das Ereignis und fährt nach Erhalt mit seiner lokalen Transaktion fort. Dies wird so lange fortgesetzt, bis alle Schritte der Transaktion abgeschlossen sind.
- Kompensierende Aktionen: Wenn ein Schritt fehlschlägt, werden kompensierende Aktionen aufgerufen. Diese Aktionen dienen dazu, die durch die vorherigen Schritte vorgenommenen Änderungen rückgängig zu machen. Wenn beispielsweise die Bestandsreduzierung nach der Zahlung fehlschlägt, würde eine Schadensersatzklage die Zahlung zurückerstatten.
2.2 Arten von Sagen
Es gibt im Wesentlichen zwei Möglichkeiten, das Saga-Muster umzusetzen: Choreographie und Orchestrierung.
2.2.1 Choreographie-Saga
In einer Choreographie-Saga gibt es keinen zentralen Koordinator. Stattdessen wartet jeder an der Saga beteiligte Dienst auf Ereignisse und entscheidet auf der Grundlage des Ergebnisses der vorherigen Schritte, wann er handelt. Dieser Ansatz ist dezentral und ermöglicht den unabhängigen Betrieb der Dienste. So funktioniert es:
- Ereignisbasierte Koordination: Jeder Dienst ist für die Abwicklung der für ihn relevanten Ereignisse verantwortlich. Nachdem der Zahlungsdienst beispielsweise eine Zahlung verarbeitet hat, gibt er ein „PaymentCompleted“-Ereignis aus. Der Inventardienst wartet auf dieses Ereignis und zieht bei Erhalt die Artikelanzahl ab.
- Dezentrale Steuerung: Da es keinen zentralen Koordinator gibt, muss jeder Dienst anhand der empfangenen Ereignisse wissen, was als nächstes zu tun ist. Dies verleiht dem System mehr Flexibilität, erfordert jedoch eine sorgfältige Planung, um sicherzustellen, dass alle Dienste die korrekte Abfolge der Vorgänge verstehen.
- Kompensierende Aktionen: Wenn ein Dienst erkennt, dass etwas schief gelaufen ist, kann er ein Fehlerereignis ausgeben, auf das andere Dienste warten, um kompensierende Aktionen auszulösen. Wenn der Inventardienst beispielsweise den Lagerbestand nicht aktualisieren kann, kann er ein „InventoryUpdateFailed“-Ereignis ausgeben, auf das der Zahlungsdienst wartet, um eine Rückerstattung auszulösen.
Vorteile der Choreographie:
- Loose Kopplung: Dienste sind lose gekoppelt, was die Skalierung und Änderung einzelner Dienste erleichtert.
- Resilienz: Da jeder Dienst unabhängig agiert, kann das System widerstandsfähiger gegenüber Ausfällen einzelner Dienste sein.
Herausforderungen der Choreographie:
- Komplexität: Mit zunehmender Anzahl von Diensten kann die Verwaltung und das Verständnis des Ablaufs von Ereignissen komplex werden.
- Fehlende zentrale Kontrolle: Ohne einen zentralen Koordinator kann es schwieriger sein, den gesamten Transaktionsfluss zu überwachen und zu debuggen.
2.2.2 Orchestrierungssaga
In einer Orchestration Saga steuert ein zentraler Orchestrator den Ablauf der Transaktion. Der Orchestrator bestimmt die Reihenfolge der Schritte und kümmert sich um die Kommunikation zwischen Diensten. So funktioniert es:
- Zentralisierte Steuerung: Der Orchestrator sendet nacheinander Befehle an jeden Dienst. Beispielsweise könnte der Orchestrator zunächst den Zahlungsdienst anweisen, eine Zahlung zu verarbeiten. Sobald dies erledigt ist, weist es den Inventardienst an, das Inventar zu aktualisieren usw.
- Sequentielle Ausführung: Jeder Dienst führt seine Aufgabe nur auf Anweisung des Orchestrators aus und stellt so sicher, dass die Schritte in der richtigen Reihenfolge ausgeführt werden.
- Kompensationslogik: Der Orchestrator ist auch dafür verantwortlich, kompensierende Maßnahmen einzuleiten, wenn etwas schief geht. Wenn beispielsweise die Bestandsaktualisierung fehlschlägt, kann der Orchestrator den Zahlungsdienst anweisen, die Zahlung zurückzuerstatten.
Vorteile der Orchestrierung:
- Zentralisierte Steuerung: Mit einem einzigen Orchestrator ist es einfacher, den Transaktionsfluss zu überwachen, zu verwalten und zu debuggen.
- Einfachere Logik: Da der Orchestrator den Ablauf verwaltet, müssen einzelne Dienste die gesamte Transaktionssequenz nicht kennen.
Herausforderungen der Orchestrierung:
- Single Point of Failure: Der Orchestrator kann zu einem Engpass oder Single Point of Failure werden, wenn er nicht für hohe Verfügbarkeit ausgelegt ist.
- Enge Kopplung mit dem Orchestrator: Dienste sind vom Orchestrator abhängig, was das System im Vergleich zur Choreografie weniger flexibel machen kann.
3. Implementierung des Simple Orchestration Saga-Musters: Eine Schritt-für-Schritt-Anleitung
Betrachten wir das E-Commerce-Szenario und implementieren es mithilfe des Saga-Musters.
In unserem Kaffeeeinkaufsszenario stellt jede Dienstleistung eine lokale Transaktion dar. Der Coffee Service fungiert als Orchestrator dieser Saga und koordiniert die anderen Dienste, um den Kauf abzuschließen.
Hier ist eine Aufschlüsselung, wie die Saga funktionieren könnte:
- Der Kunde gibt eine Bestellung auf: Der Kunde gibt eine Bestellung über den Bestellservice auf.
- Kaffeeservice leitet die Saga ein: Der Kaffeeservice erhält die Bestellung und leitet die Saga ein.
- Bestellservice erstellt eine Bestellung: Der Bestellservice erstellt eine neue Bestellung und behält diese bei.
- Der Abrechnungsdienst berechnet die Kosten: Der Abrechnungsdienst berechnet die Gesamtkosten der Bestellung und erstellt einen Abrechnungsdatensatz.
- Zahlungsdienst verarbeitet die Zahlung: Der Zahlungsdienst verarbeitet die Zahlung.
- Coffee Service aktualisiert den Bestellstatus: Sobald die Zahlung erfolgreich ist, aktualisiert der Coffee Service den Bestellstatus auf „abgeschlossen“.
3.1 Transaktionseinheit
In meiner Implementierung der Saga stellt jeder SagaItemBuilder einen Schritt in unserem verteilten Transaktionsfluss dar. Der ActionBuilder definiert die auszuführenden Aktionen, einschließlich der Hauptaktion und der Rollback-Aktion, die ausgeführt wird, wenn ein Fehler auftritt. Der ActionBuilder kapselt drei Informationen:
component : Die Bean-Instanz, in der sich die aufzurufende Methode befindet.
Methode: Der Name der aufzurufenden Methode.
args: Die Argumente, die an die Methode übergeben werden sollen.
ActionBuilder
public class ActionBuilder { private Object component; private String method; private Object[] args; public static ActionBuilder builder() { return new ActionBuilder(); } public ActionBuilder component(Object component) { this.component = component; return this; } public ActionBuilder method(String method) { this.method = method; return this; } public ActionBuilder args(Object... args) { this.args = args; return this; } public Object getComponent() { return component; } public String getMethod() { return method; } public Object[] getArgs() { return args; } }
SagaItemBuilder
import java.util.HashMap; import java.util.Map; import java.util.Objects; public class SagaItemBuilder { private ActionBuilder action; private Map<class extends exception>, ActionBuilder> onBehaviour; public static SagaItemBuilder builder() { return new SagaItemBuilder(); } public SagaItemBuilder action(ActionBuilder action) { this.action = action; return this; } public SagaItemBuilder onBehaviour(Class extends Exception> exception, ActionBuilder action) { if (Objects.isNull(onBehaviour)) onBehaviour = new HashMap(); onBehaviour.put(exception, action); return this; } public ActionBuilder getAction() { return action; } public Map<class extends exception>, ActionBuilder> getBehaviour() { return onBehaviour; } } </class></class>
Szenarien
import java.util.ArrayList; import java.util.List; public class Scenarios { List<sagaitembuilder> scenarios; public static Scenarios builder() { return new Scenarios(); } public Scenarios scenario(SagaItemBuilder sagaItemBuilder) { if (scenarios == null) scenarios = new ArrayList(); scenarios.add(sagaItemBuilder); return this; } public List<sagaitembuilder> getScenario() { return scenarios; } } </sagaitembuilder></sagaitembuilder>
Unten erfahren Sie, wie ich die Verteilungstransaktion festschreiben kann.
package com.example.demo.saga; import com.example.demo.saga.exception.CanNotRollbackException; import com.example.demo.saga.exception.RollBackException; import com.example.demo.saga.pojo.ActionBuilder; import com.example.demo.saga.pojo.SagaItemBuilder; import com.example.demo.saga.pojo.Scenarios; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Map; import java.util.Set; @Component public class DTC { public boolean commit(Scenarios scenarios) throws Exception { validate(scenarios); for (int i = 0; i = 0; i--) { SagaItemBuilder scenario = scenarios.getScenario().get(i); Map<class extends exception>, ActionBuilder> behaviours = scenario.getBehaviour(); Set<class extends exception>> exceptions = behaviours.keySet(); ActionBuilder actionWhenException = null; if (failStep == i) { for(Class extends Exception> exception: exceptions) { if (exception.isInstance(currentStepFailException)) { actionWhenException = behaviours.get(exception); } } if (actionWhenException == null) actionWhenException = behaviours.get(RollBackException.class); } else { actionWhenException = behaviours.get(RollBackException.class); } Object bean = actionWhenException.getComponent(); String method = actionWhenException.getMethod(); Object[] args = actionWhenException.getArgs(); try { invoke(bean, method, args); } catch (Exception e) { throw new CanNotRollbackException("Error in %s belong to %s. Can not rollback transaction".formatted(method, bean.getClass())); } } } private void validate(Scenarios scenarios) throws Exception { for (int i = 0; i , ActionBuilder> behaviours = scenario.getBehaviour(); Set<class extends exception>> exceptions = behaviours.keySet(); if (exceptions.contains(null)) throw new Exception("Exception can not be null in scenario has method %s, bean %s " .formatted(action.getMethod(), action.getComponent().getClass())); if (!exceptions.contains(RollBackException.class)) throw new Exception("Missing default RollBackException in scenario has method %s, bean %s " .formatted(action.getMethod(), action.getComponent().getClass())); } } public String invoke(Object bean, String methodName, Object... args) throws Exception { try { Class>[] paramTypes = new Class[args.length]; for (int i = 0; i parameterType (Object o) { if (o instanceof Integer) { return int.class; } else if (o instanceof Boolean) { return boolean.class; } else if (o instanceof Double) { return double.class; } else if (o instanceof Float) { return float.class; } else if (o instanceof Long) { return long.class; } else if (o instanceof Short) { return short.class; } else if (o instanceof Byte) { return byte.class; } else if (o instanceof Character) { return char.class; } else { return o.getClass(); } } } </class></class></class>
3.2 Verwendung
Ich habe 3 Dienste, die externe Dienste aufrufen: BillingService, OrderService, PaymentService.
Bestellservice
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class OrderService { public String prepareOrder(String name, int number) { System.out.println("Prepare order for %s with order id %d ".formatted(name, number)); return "Prepare order for %s with order id %d ".formatted(name, number); } public void Rollback_prepareOrder_NullPointException() { System.out.println("Rollback prepareOrder because NullPointException"); } public void Rollback_prepareOrder_RollBackException() { System.out.println("Rollback prepareOrder because RollBackException"); } }
Abrechnungsservice
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class BillingService { public String prepareBilling(String name, int number) { System.out.println("Prepare billing for %s with order id %d ".formatted(name, number)); return "Prepare billing for %s with order id %d ".formatted(name, number); } public String createBilling(String name, int number) { System.out.println("Create billing for %s with order id %d ".formatted(name, number)); return "Create billing for %s with order id %d ".formatted(name, number); } public void Rollback_prepareBilling_NullPointException() { System.out.println("Rollback prepareBilling because NullPointException"); } public void Rollback_prepareBilling_ArrayIndexOutOfBoundsException() { System.out.println("Rollback prepareBilling because ArrayIndexOutOfBoundsException"); } public void Rollback_prepareBilling_RollBackException() { System.out.println("Rollback prepareBilling because RollBackException"); } public void Rollback_createBilling_NullPointException() { System.out.println("Rollback createBilling because NullPointException"); } public void Rollback_createBilling_ArrayIndexOutOfBoundsException() { System.out.println("Rollback createBilling because ArrayIndexOutOfBoundsException"); } public void Rollback_createBilling_RollBackException() { System.out.println("Rollback createBilling because RollBackException"); } }
Zahlungsservice
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class PaymentService { public String createPayment() { System.out.println("Create payment"); return "Create payment"; } public void Rollback_createPayment_NullPointException() { System.out.println("Rollback createPayment because NullPointException"); } public void Rollback_createPayment_RollBackException() { System.out.println("Rollback createPayment because RollBackException"); } }
Und im Coffee Service setze ich es wie folgt um: Ich erstelle ein Szenario und schreibe es dann fest.
package com.example.demo.service; import com.example.demo.saga.DTC; import com.example.demo.saga.exception.RollBackException; import com.example.demo.saga.pojo.ActionBuilder; import com.example.demo.saga.pojo.SagaItemBuilder; import com.example.demo.saga.pojo.Scenarios; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CoffeeService { @Autowired private OrderService orderService; @Autowired private BillingService billingService; @Autowired private PaymentService paymentService; @Autowired private DTC dtc; public String test() throws Exception { Scenarios scenarios = Scenarios.builder() .scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(orderService).method("prepareOrder").args("tuanh.net", 123)) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(orderService).method("Rollback_prepareOrder_NullPointException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(orderService).method("Rollback_prepareOrder_RollBackException").args()) ).scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(billingService).method("prepareBilling").args("tuanh.net", 123)) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(billingService).method("Rollback_prepareBilling_NullPointException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(billingService).method("Rollback_prepareBilling_RollBackException").args()) ).scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(billingService).method("createBilling").args("tuanh.net", 123)) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(billingService).method("Rollback_createBilling_ArrayIndexOutOfBoundsException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(billingService).method("Rollback_createBilling_RollBackException").args()) ).scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(paymentService).method("createPayment").args()) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(paymentService).method("Rollback_createPayment_NullPointException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(paymentService).method("Rollback_createPayment_RollBackException").args()) ); dtc.commit(scenarios); return "ok"; } }
3.3 Ergebnis
Wenn ich beim Erstellen einer Abrechnung eine Ausnahme mache.
public String createBilling(String name, int number) { throw new NullPointerException(); }
Ergebnis
2024-08-24T14:21:45.445+07:00 INFO 19736 --- [demo] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2024-08-24T14:21:45.450+07:00 INFO 19736 --- [demo] [main] com.example.demo.DemoApplication : Started DemoApplication in 1.052 seconds (process running for 1.498) 2024-08-24T14:21:47.756+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2024-08-24T14:21:47.756+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2024-08-24T14:21:47.757+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms Prepare order for tuanh.net with order id 123 Prepare billing for tuanh.net with order id 123 Rollback createBilling because RollBackException Rollback prepareBilling because RollBackException Rollback prepareOrder because RollBackException
Schauen Sie sich mein GitHub-Repository an
4. Fazit
Zusammenfassend lässt sich sagen, dass das Saga-Muster eine robuste Lösung für die Verwaltung verteilter Transaktionen bietet, indem es diese in kleinere, überschaubare Schritte unterteilt. Die Wahl zwischen Choreografie und Orchestrierung hängt von den spezifischen Anforderungen und der Architektur Ihres Systems ab. Choreografie bietet lose Kopplung und Ausfallsicherheit, während Orchestrierung eine zentrale Steuerung und einfachere Überwachung ermöglicht. Indem Sie Ihr System sorgfältig mit dem Saga-Muster entwerfen, können Sie Konsistenz, Verfügbarkeit und Flexibilität in Ihrer verteilten Microservices-Architektur erreichen.
Kommentieren Sie unten, wenn Sie Fragen haben oder weitere Erläuterungen zur Implementierung des Saga-Musters in Ihrem System benötigen!
Weitere Beiträge finden Sie unter: Wie das Saga-Muster verteilte Transaktionsprobleme löst: Methoden und Beispiele aus der Praxis
Das obige ist der detaillierte Inhalt vonWie das Saga-Muster verteilte Transaktionsprobleme löst: Methoden und Beispiele aus der Praxis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

In dem Artikel werden Maven und Gradle für Java -Projektmanagement, Aufbau von Automatisierung und Abhängigkeitslösung erörtert, die ihre Ansätze und Optimierungsstrategien vergleichen.

In dem Artikel werden benutzerdefinierte Java -Bibliotheken (JAR -Dateien) mit ordnungsgemäßem Versioning- und Abhängigkeitsmanagement erstellt und verwendet, wobei Tools wie Maven und Gradle verwendet werden.

In dem Artikel wird in der Implementierung von mehrstufigem Caching in Java mithilfe von Koffein- und Guava-Cache zur Verbesserung der Anwendungsleistung erläutert. Es deckt die Einrichtungs-, Integrations- und Leistungsvorteile sowie die Bestrafung des Konfigurations- und Räumungsrichtlinienmanagements ab

In dem Artikel werden mit JPA für Objektrelationszuordnungen mit erweiterten Funktionen wie Caching und faulen Laden erläutert. Es deckt Setup, Entity -Mapping und Best Practices zur Optimierung der Leistung ab und hebt potenzielle Fallstricke hervor. [159 Charaktere]

Mit der Klassenbelastung von Java wird das Laden, Verknüpfen und Initialisieren von Klassen mithilfe eines hierarchischen Systems mit Bootstrap-, Erweiterungs- und Anwendungsklassenloadern umfasst. Das übergeordnete Delegationsmodell stellt sicher


Heiße KI -Werkzeuge

Undresser.AI Undress
KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover
Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Undress AI Tool
Ausziehbilder kostenlos

Clothoff.io
KI-Kleiderentferner

AI Hentai Generator
Erstellen Sie kostenlos Ai Hentai.

Heißer Artikel

Heiße Werkzeuge

Dreamweaver CS6
Visuelle Webentwicklungstools

Sicherer Prüfungsbrowser
Safe Exam Browser ist eine sichere Browserumgebung für die sichere Teilnahme an Online-Prüfungen. Diese Software verwandelt jeden Computer in einen sicheren Arbeitsplatz. Es kontrolliert den Zugriff auf alle Dienstprogramme und verhindert, dass Schüler nicht autorisierte Ressourcen nutzen.

SublimeText3 Linux neue Version
SublimeText3 Linux neueste Version

MantisBT
Mantis ist ein einfach zu implementierendes webbasiertes Tool zur Fehlerverfolgung, das die Fehlerverfolgung von Produkten unterstützen soll. Es erfordert PHP, MySQL und einen Webserver. Schauen Sie sich unsere Demo- und Hosting-Services an.

WebStorm-Mac-Version
Nützliche JavaScript-Entwicklungstools