Sprechen Sie über die Anwendung der reaktiven Programmierung auf dem Server, die Optimierung des Datenbankbetriebs von 20 Sekunden bis 0,5 Sekunden

Die Anwendung der reaktiven Programmierung in der Client-Programmierung ist ziemlich umfangreich, während die aktuellen Anwendungen auf dem Server relativ wenig erwähnt werden. In diesem Artikel wird erläutert, wie Sie die Antwortzeitprogrammierung in der serverseitigen Programmierung verwenden, um die Leistung von Datenbankoperationen zu verbessern.

Der Anfang ist der Abschluss

Mit System.Reactive mit TaskCompelteSource ist es möglich, verstreute Einfügeanforderungen für einzelne Datenbanken in einer Stapeleinfügeanforderung zu kombinieren. Unter der Voraussetzung der Gewährleistung der Korrektheit wird die Optimierung der Leistung beim Einfügen von Datenbanken realisiert.

Wenn der Leser bereits weiß, wie er zu bedienen ist, muss der Rest des Inhalts nicht gelesen werden.

Voraussetzung

Wir gehen nun davon aus, dass es eine solche Repository-Schnittstelle gibt, die eine Datenbankeinfügeoperation darstellt.

csharp

namespace Newbe.RxWorld.DatabaseRepository
{    public interface IDatabaseRepository
    {        /// <summary>
        /// Insert one item and return total count of data in database
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        Task<int> InsertData(int item);
    }
}

Lassen Sie uns als Nächstes den Leistungsunterschied erleben, der durch verschiedene Implementierungen entsteht, ohne die Schnittstellensignatur zu ändern.

Basisversion

Die erste ist die Basisversion, die die konventionellste INSERT-Operation für eine einzelne Datenbank verwendet, um das Einfügen der Daten abzuschließen. In diesem Beispiel wird SQLite als Demonstrationsdatenbank verwendet, mit der Leser bequem selbst experimentieren können.

csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl
{    public class NormalDatabaseRepository : IDatabaseRepository
    {        private readonly IDatabase _database;
        public NormalDatabaseRepository(
            IDatabase database)
        {            _database = database;        }        public Task<int> InsertData(int item)
        {            return _database.InsertOne(item);
        }    }}

Routinebetrieb. Die spezifische Implementierung von _database.InsertOne (item) besteht darin, INSERT einmal aufzurufen.

Die Basisversion kann grundsätzlich schneller fertiggestellt werden, wenn weniger als 20 Mal gleichzeitig eingefügt wird. Wenn sich beispielsweise die Größenordnung erhöht, dauert es ungefähr 20 Sekunden, um 10.000 Datenbanken gleichzeitig einzufügen, und es gibt viel Raum für Optimierungen.

TaskCompelteSource

TaskCompelteSource ist ein Typ, der eine operative Task in der TPL-Bibliothek generieren kann. Leser, die mit TaskCompelteSource nicht vertraut sind, können aus diesem Beispielcode lernen.

Hier finden Sie auch eine kurze Erläuterung der Rolle des Objekts, damit die Leser weiterlesen können.

Für Freunde, die mit Javascript vertraut sind, kann TaskCompelteSource als einem Promise-Objekt gleichwertig angesehen werden. Es kann auch $ .Deferred in jQuery entsprechen.

Wenn Sie nichts darüber wissen, können Sie sich die lebensechten Beispiele anhören, an die ich gedacht habe, als ich Mala Tang gegessen habe.

Sprechen Sie über die Anwendung der reaktiven Programmierung auf dem Server, die Optimierung des Datenbankbetriebs von 20 Sekunden bis 0,5 Sekunden

 

Das Essen der Mala Tang-Technologie erklärt, dass Sie vor dem Essen des Mala Tang einen Teller verwenden müssen, um das Geschirr einzuschließen. Nachdem Sie die Parameter konstruiert und das Essen festgeklemmt haben, bringen Sie es zur Kasse und rufen Sie die Methode auf. Nachdem der Kassierer die Kasse beendet hat, erhält der Kassierer eine Bestellkarte, die klingelt und einen Aufgabenrückgabewert erhält. Nehmen Sie die Menükarte und suchen Sie sich einen Platz zum Sitzen. Wenn Sie am Telefon spielen und auf eine Mahlzeit warten, wartet diese Aufgabe auf die CPU. Das Menü klingelt, holt die Mahlzeit und die Aufgabe ist abgeschlossen, warten Sie auf die Anzahl der Abschnitte und führen Sie die nächste Codezeile aus

Wo ist TaskCompelteSource?

Zunächst werden wir gemäß dem obigen Beispiel die Mahlzeit nur abholen, wenn das Menü klingelt. Wann klingelt das Menü? Natürlich drückte der Kellner manuell einen Handschalter am Zähler, um die Glocke auszulösen.

Dieser Schalter am Zähler kann dann technisch als TaskCompelteSource interpretiert werden.

Der Tischschalter kann die Glocke des Menüs steuern. Ebenso ist TaskCompelteSource ein Objekt, das den Status von Task steuern kann.

Lösungen

Mit dem vorherigen Verständnis von TaskCompelteSource kann das Problem am Anfang des Artikels gelöst werden. Die Idee ist wie folgt:

Wenn InsertData aufgerufen wird, kann ein Tupel aus TaskCompelteSource und Element erstellt werden. Zur Vereinfachung der Erklärung haben wir dieses Tupel BatchItem genannt.

Geben Sie die Task zurück, die der TaskCompelteSource des BatchItem entspricht.

Der Code, der InsertData aufruft, wartet auf die zurückgegebene Task. Solange die TaskCompelteSource nicht ausgeführt wird, wartet der Aufrufer eine Weile.

Anschließend wird ein anderer Thread gestartet, der die BatchItem-Warteschlange regelmäßig belegt.

Damit ist der Vorgang des Verwandelns eines einzelnen Einsatzes in einen Stapeleinsatz abgeschlossen.

Der Autor erklärt möglicherweise nicht klar, aber alle folgenden Versionen des Codes basieren auf den oben genannten Ideen. Leser können Text und Code kombinieren, um zu verstehen.

ConcurrentQueue-Version

Basierend auf den oben genannten Ideen verwenden wir ConcurrentQueue als BatchItem-Warteschlange für die Implementierung. Der Code lautet wie folgt (es gibt viel Code, keine Sorge, da es unten einfachere gibt):

 

csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl
{    public class ConcurrentQueueDatabaseRepository : IDatabaseRepository    {        private readonly ITestOutputHelper _testOutputHelper;
        private readonly IDatabase _database;
        private readonly ConcurrentQueue<BatchItem> _queue;
        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        private readonly Task _batchInsertDataTask;
        public ConcurrentQueueDatabaseRepository(            ITestOutputHelper testOutputHelper,            IDatabase database)        {            _testOutputHelper = testOutputHelper;            _database = database;            _queue = new ConcurrentQueue<BatchItem>();            // 启动一个 Task 消费队列中的 BatchItem            _batchInsertDataTask = Task.Factory.StartNew(RunBatchInsert, TaskCreationOptions.LongRunning);            _batchInsertDataTask.ConfigureAwait(false);
        }        public Task<int> InsertData(int item)        {            // 生成 BatchItem ,将对象放入队列。返回 Task 出去            var taskCompletionSource = new TaskCompletionSource<int>();            _queue.Enqueue(new BatchItem            {                Item = item,                TaskCompletionSource = taskCompletionSource            });            return taskCompletionSource.Task;
        }        // 从队列中不断获取 BatchItem ,并且一批一批插入数据库,更新 TaskCompletionSource 的状态        private void RunBatchInsert()
        {            foreach (var batchItems in GetBatches())
            {                try                {                    BatchInsertData(batchItems).Wait();                }                catch (Exception e)                {                    _testOutputHelper.WriteLine($"there is an error : {e}");
                }            }            IEnumerable<IList<BatchItem>> GetBatches()
            {                var sleepTime = TimeSpan.FromMilliseconds(50);                while (true)
                {                    const int maxCount = 100;                    var oneBatchItems = GetWaitingItems()                        .Take(maxCount)                        .ToList();                    if (oneBatchItems.Any())
                    {                        yield return oneBatchItems;
                    }                    else
                    {                        Thread.Sleep(sleepTime);                    }                }                IEnumerable<BatchItem> GetWaitingItems()
                {                    while (_queue.TryDequeue(out var item))
                    {                        yield return item;
                    }                }            }        }        private async Task BatchInsertData(IEnumerable<BatchItem> items)        {            var batchItems = items as BatchItem[] ?? items.ToArray();            try            {                // 调用数据库的批量插入操作                var totalCount = await _database.InsertMany(batchItems.Select(x => x.Item));                foreach (var batchItem in batchItems)
                {                    batchItem.TaskCompletionSource.SetResult(totalCount);                }            }            catch (Exception e)            {                foreach (var batchItem in batchItems)
                {                    batchItem.TaskCompletionSource.SetException(e);                }                throw;            }        }        private struct BatchItem        {            public TaskCompletionSource<int> TaskCompletionSource { get; set; }
            public int Item { get; set; }
        }    }}

Weitere lokale Funktionen und IEnumerable-Funktionen werden im obigen Code verwendet. Leser, die dies nicht verstehen, können hier klicken, um es zu verstehen.

Der Spielfilm beginnt!

Als nächstes verwenden wir System.Reactive, um die komplexere Version von ConcurrentQueue oben zu transformieren. wie folgt:

 

csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl
{    public class AutoBatchDatabaseRepository : IDatabaseRepository
    {        private readonly ITestOutputHelper _testOutputHelper;
        private readonly IDatabase _database;
        private readonly Subject<BatchItem> _subject;
        public AutoBatchDatabaseRepository(
            ITestOutputHelper testOutputHelper,            IDatabase database)        {            _testOutputHelper = testOutputHelper;            _database = database;            _subject = new Subject<BatchItem>();
            // 将请求进行分组,每50毫秒一组或者每100个一组
            _subject.Buffer(TimeSpan.FromMilliseconds(50), 100)
                .Where(x => x.Count > 0)
                // 将每组数据调用批量插入,写入数据库
                .Select(list => Observable.FromAsync(() => BatchInsertData(list)))
                .Concat()
                .Subscribe();
        }
        // 这里和前面对比没有变化
        public Task<int> InsertData(int item)
        {
            var taskCompletionSource = new TaskCompletionSource<int>();
            _subject.OnNext(new BatchItem
            {
                Item = item,
                TaskCompletionSource = taskCompletionSource
            });
            return taskCompletionSource.Task;
        }
        // 这段和前面也完全一样,没有变化
        private async Task BatchInsertData(IEnumerable<BatchItem> items)
        {
            var batchItems = items as BatchItem[] ?? items.ToArray();
            try
            {
                var totalCount = await _database.InsertMany(batchItems.Select(x => x.Item));
                foreach (var batchItem in batchItems)
                {
                    batchItem.TaskCompletionSource.SetResult(totalCount);
                }
            }
            catch (Exception e)
            {
                foreach (var batchItem in batchItems)
                {
                    batchItem.TaskCompletionSource.SetException(e);
                }
                throw;
            }
        }
        private struct BatchItem
        {
            public TaskCompletionSource<int> TaskCompletionSource { get; set; }
            public int Item { get; set; }
        }
    }
}

Der Code wird um 50 Zeilen reduziert. Der Hauptgrund ist die Verwendung der in System.Reactive bereitgestellten leistungsstarken Puffermethode, um die komplexe Logikimplementierung in der ConcurrentQueue-Version zu implementieren.

Lehrer, können Sie ein bisschen stärker sein?

Wir können den Code "leicht" optimieren, um den Puffer und die zugehörige Logik unabhängig von der Geschäftslogik der "Datenbankeinfügung" zu machen. Dann bekommen wir eine einfachere Version:

 

csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl
{    public class FinalDatabaseRepository : IDatabaseRepository
    {        private readonly IBatchOperator<int, int> _batchOperator;
        public FinalDatabaseRepository(
            IDatabase database)
        {            var options = new BatchOperatorOptions<int, int>
            {                BufferTime = TimeSpan.FromMilliseconds(50),
                BufferCount = 100,
                DoManyFunc = database.InsertMany,            };            _batchOperator = new BatchOperator<int, int>(options);
        }        public Task<int> InsertData(int item)
        {            return _batchOperator.CreateTask(item);
        }    }}

Unter IBatchOperator und anderen Codes können Leser zur Codebasis gehen, um sie anzuzeigen. Sie wird hier nicht angezeigt.

Leistungstest

Grundsätzlich kann es wie folgt gemessen werden:

Wenn 10 Daten gleichzeitig ausgeführt werden, gibt es keinen großen Unterschied zwischen der Originalversion und der Stapelversion. Selbst die Batch-Version ist langsamer, wenn die Anzahl klein ist, schließlich gibt es eine maximale Wartezeit von 50 Millisekunden.

Wenn jedoch ein Stapelvorgang erforderlich ist, um gleichzeitig 10.000 Daten zu verarbeiten, kann die ursprüngliche Version 20 Sekunden dauern, während die Stapelversion nur 0,5 Sekunden dauert.

Alle Beispielcodes finden Sie in der Codebibliothek. Wenn der Github-Klon schwierig ist, können Sie auch hier klicken, um von Gitee zu klonen

Vor allem aber!

Vor kurzem hat der Autor eine Reihe von serverseitigen Entwicklungsframeworks erstellt, die auf der Rückverfolgbarkeit von Reaktivfunktionen, Akteursmodus und Ereignissen basieren. Ich hoffe, Entwicklern ein Anwendungssystem zur Verfügung stellen zu können, das die Entwicklung von "verteilt", "horizontal skalierbar" und "hoher Testbarkeit" -Newbe.Claptrap erleichtert

Ich denke du magst

Origin blog.csdn.net/qq_45401061/article/details/108720890
Empfohlen
Rangfolge