.NET Core實現簡單的Redis Client框架
0,關於 Redis RESP
RESP 全稱 REdis Serialization Protocol ,即 Redis 序列化協議,用於協定客戶端使用 socket 連接 Redis 時,數據的傳輸規則。
官方協議說明:https://redis.io/topics/protocol
那麼 RESP 協議在與 Redis 通訊時的 請求-響應 方式如下:
- 客戶端將命令作為 RESP 大容量字符串數組(即 C# 中使用 byte[] 存儲字符串命令)發送到 Redis 服務器。
- 服務器根據命令實現以 RESP 類型進行回復。
RESP 中的類型並不是指 Redis 的基本數據類型,而是指數據的響應格式:
在 RESP 中,某些數據的類型取決於第一個字節:
- 對於簡單字符串,答復的第一個字節為“ +”
- 對於錯誤,回復的第一個字節為“-”
- 對於整數,答復的第一個字節為“:”
- 對於批量字符串,答復的第一個字節為“ $”
- 對於數組,回復的第一個字節為“
*
”
對於這些,可能初學者不太瞭解,下面我們來實際操作一下。
我們打開 Redis Desktop Manager ,然後點擊控制臺,輸入:
set a 12 set b 12 set c 12 MGET abc
以上命令每行按一下回車鍵。MGET 是 Redis 中一次性取出多個鍵的值的命令。
輸出結果如下:
本地:0>SET a 12 "OK" 本地:0>SET b 12 "OK" 本地:0>SET c 12 "OK" 本地:0>MGET a b c 1) "12" 2) "12" 3) "12"
但是這個管理工具以及去掉瞭 RESP 中的協議標識符,我們來寫一個 demo 代碼,還原 RESP 的本質。
using System; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp { class Program { static async Task Main(string[] args) { IPAddress IP = IPAddress.Parse("127.0.0.1"); IPEndPoint IPEndPoint = new IPEndPoint(IP, 6379); Socket client = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); await client.ConnectAsync(IPEndPoint); if (!client.Connected) { Console.WriteLine("連接 Redis 服務器失敗!"); Console.Read(); } Console.WriteLine("恭喜恭喜,連接 Redis 服務器成功"); // 後臺接收消息 new Thread(() => { while (true) { byte[] data = new byte[100]; int size = client.Receive(data); Console.WriteLine(); Console.WriteLine(Encoding.UTF8.GetString(data)); Console.WriteLine(); } }).Start(); while (true) { Console.Write("$> "); string command = Console.ReadLine(); // 發送的命令必須以 \r\n 結尾 int size = client.Send(Encoding.UTF8.GetBytes(command + "\r\n")); Thread.Sleep(100); } } } }
輸入以及輸出結果:
$> SET a 123456789 +OK $> SET b 123456789 +OK $> SET c 123456789 +OK $> MGET a b c *3 $9 123456789 $9 123456789 $9 123456789
可見,Redis 響應的消息內容,是以 $、*、+ 等字符開頭的,並且使用 \r\n 分隔。
我們寫 Redis Client 的方法就是接收 socket 內容,然後從中解析出實際的數據。
每次發送設置命令成功,都會返回 +OK;*3 表示有三個數組;$9 表示接收的數據長度是 9;
大概就是這樣瞭,下面我們來寫一個簡單的 Redis Client 框架,然後睡覺。
記得使用 netstandard2.1,因為有些 byte[] 、string、ReadOnlySpan<T>
的轉換,需要 netstandard2.1 才能更加方便。
1,定義數據類型
根據前面的 demo,我們來定義一個類型,存儲那些特殊符號:
/// <summary> /// RESP Response 類型 /// </summary> public static class RedisValueType { public const byte Errors = (byte)'-'; public const byte SimpleStrings = (byte)'+'; public const byte Integers = (byte)':'; public const byte BulkStrings = (byte)'$'; public const byte Arrays = (byte)'*'; public const byte R = (byte)'\r'; public const byte N = (byte)'\n'; }
2,定義異步消息狀態機
創建一個 MessageStrace 類,作用是作為消息響應的異步狀態機,並且具有解析數據流的功能。
/// <summary> /// 自定義消息隊列狀態機 /// </summary> public abstract class MessageStrace { protected MessageStrace() { TaskCompletionSource = new TaskCompletionSource<string>(); Task = TaskCompletionSource.Task; } protected readonly TaskCompletionSource<string> TaskCompletionSource; /// <summary> /// 標志任務是否完成,並接收 redis 響應的字符串數據流 /// </summary> public Task<string> Task { get; private set; } /// <summary> /// 接收數據流 /// </summary> /// <param name="stream"></param> /// <param name="length">實際長度</param> public abstract void Receive(MemoryStream stream, int length); /// <summary> /// 響應已經完成 /// </summary> /// <param name="data"></param> protected void SetValue(string data) { TaskCompletionSource.SetResult(data); } /// <summary> /// 解析 $ 或 * 符號後的數字,必須傳遞符後後一位的下標 /// </summary> /// <param name="data"></param> /// <param name="index">解析到的位置</param> /// <returns></returns> protected int BulkStrings(ReadOnlySpan<byte> data, ref int index) { int start = index; int end = start; while (true) { if (index + 1 >= data.Length) throw new ArgumentOutOfRangeException("溢出"); // \r\n if (data[index].CompareTo(RedisValueType.R) == 0 && data[index + 1].CompareTo(RedisValueType.N) == 0) { index += 2; // 指向 \n 的下一位 break; } end++; index++; } // 截取 $2 *3 符號後面的數字 return Convert.ToInt32(Encoding.UTF8.GetString(data.Slice(start, end - start).ToArray())); } }
3,定義命令發送模板
由於 Redis 命令非常多,為瞭更加好的封裝,我們定義一個消息發送模板,規定五種類型分別使用五種類型發送 Client。
定義一個統一的模板類:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace CZGL.RedisClient { /// <summary> /// 命令發送模板 /// </summary> public abstract class CommandClient<T> where T : CommandClient<T> { protected RedisClient _client; protected CommandClient() { } protected CommandClient(RedisClient client) { _client = client; } /// <summary> /// 復用 /// </summary> /// <param name="client"></param> /// <returns></returns> internal virtual CommandClient<T> Init(RedisClient client) { _client = client; return this; } /// <summary> /// 請求是否成功 /// </summary> /// <param name="value">響應的消息</param> /// <returns></returns> protected bool IsOk(string value) { if (value[0].CompareTo('+') != 0 || value[1].CompareTo('O') != 0 || value[2].CompareTo('K') != 0) return false; return true; } /// <summary> /// 發送命令 /// </summary> /// <param name="command">發送的命令</param> /// <param name="strace">數據類型客戶端</param> /// <returns></returns> protected Task SendCommand<TStrace>(string command, out TStrace strace) where TStrace : MessageStrace, new() { strace = new TStrace(); return _client.SendAsync(strace, command); } } }
4,定義 Redis Client
RedisClient 類用於發送 Redis 命令,然後將任務放到隊列中;接收 Redis 返回的數據內容,並將數據流寫入內存中,調出隊列,設置異步任務的返回值。
Send 過程可以並發,但是接收消息內容使用單線程。為瞭保證消息的順序性,采用隊列來記錄 Send – Receive 的順序。
C# 的 Socket 比較操蛋,想搞並發和高性能 Socket 不是那麼容易。
以下代碼有三個地方註釋瞭,後面繼續編寫其它代碼會用到。
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; namespace CZGL.RedisClient { /// <summary> /// Redis 客戶端 /// </summary> public class RedisClient { private readonly IPAddress IP; private readonly IPEndPoint IPEndPoint; private readonly Socket client; //private readonly Lazy<StringClient> stringClient; //private readonly Lazy<HashClient> hashClient; //private readonly Lazy<ListClient> listClient; //private readonly Lazy<SetClient> setClient; //private readonly Lazy<SortedClient> sortedClient; // 數據流請求隊列 private readonly ConcurrentQueue<MessageStrace> StringTaskQueue = new ConcurrentQueue<MessageStrace>(); public RedisClient(string ip, int port) { IP = IPAddress.Parse(ip); IPEndPoint = new IPEndPoint(IP, port); //stringClient = new Lazy<StringClient>(() => new StringClient(this)); //hashClient = new Lazy<HashClient>(() => new HashClient(this)); //listClient = new Lazy<ListClient>(() => new ListClient(this)); //setClient = new Lazy<SetClient>(() => new SetClient(this)); //sortedClient = new Lazy<SortedClient>(() => new SortedClient(this)); client = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); } /// <summary> /// 開始連接 Redis /// </summary> public async Task<bool> ConnectAsync() { await client.ConnectAsync(IPEndPoint); new Thread(() => { ReceiveQueue(); }) { IsBackground = true }.Start(); return client.Connected; } /// <summary> /// 發送一個命令,將其加入隊列 /// </summary> /// <param name="task"></param> /// <param name="command"></param> /// <returns></returns> internal Task<int> SendAsync(MessageStrace task, string command) { var buffer = Encoding.UTF8.GetBytes(command + "\r\n"); var result = client.SendAsync(new ArraySegment<byte>(buffer, 0, buffer.Length), SocketFlags.None); StringTaskQueue.Enqueue(task); return result; } /* Microsoft 對緩沖區輸入不同大小的數據,測試響應時間。 1024 - real 0m0,102s; user 0m0,018s; sys 0m0,009s 2048 - real 0m0,112s; user 0m0,017s; sys 0m0,009s 8192 - real 0m0,163s; user 0m0,017s; sys 0m0,007s 256 - real 0m0,101s; user 0m0,019s; sys 0m0,008s 16 - real 0m0,144s; user 0m0,016s; sys 0m0,010s .NET Socket,默認緩沖區的大小為 8192 字節。 Socket.ReceiveBufferSize: An Int32 that contains the size, in bytes, of the receive buffer. The default is 8192. 但響應中有很多隻是 "+OK\r\n" 這樣的響應,並且 MemoryStream 剛好默認是 256(當然,可以自己設置大小),緩沖區過大,浪費內存; 超過 256 這個大小,MemoryStream 會繼續分配新的 256 大小的內存區域,會消耗性能。 BufferSize 設置為 256 ,是比較合適的做法。 */ private const int BufferSize = 256; /// <summary> /// 單線程串行接收數據流,調出任務隊列完成任務 /// </summary> private void ReceiveQueue() { while (true) { MemoryStream stream = new MemoryStream(BufferSize); // 內存緩存區 byte[] data = new byte[BufferSize]; // 分片,每次接收 N 個字節 int size = client.Receive(data); // 等待接收一個消息 int length = size; // 數據流總長度 while (true) { stream.Write(data, 0, size); // 分片接收的數據流寫入內存緩沖區 // 數據流接收完畢 if (size < BufferSize) // 存在 Bug ,當數據流的大小或者數據流分片最後一片的字節大小剛剛好為 BufferSize 大小時,無法跳出 Receive { break; } length += client.Receive(data); // 還沒有接收完畢,繼續接收 } stream.Seek(0, SeekOrigin.Begin); // 重置遊標位置 // 調出隊列 StringTaskQueue.TryDequeue(out var tmpResult); // 處理隊列中的任務 tmpResult.Receive(stream, length); } } /// <summary> /// 復用 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="client"></param> /// <returns></returns> public T GetClient<T>(T client) where T : CommandClient<T> { client.Init(this); return client; } ///// <summary> ///// 獲取字符串請求客戶端 ///// </summary> ///// <returns></returns> //public StringClient GetStringClient() //{ // return stringClient.Value; //} //public HashClient GetHashClient() //{ // return hashClient.Value; //} //public ListClient GetListClient() //{ // return listClient.Value; //} //public SetClient GetSetClient() //{ // return setClient.Value; //} //public SortedClient GetSortedClient() //{ // return sortedClient.Value; //} } }
5,實現簡單的 RESP 解析
下面使用代碼來實現對 Redis RESP 消息的解析,時間問題,我隻實現 +、-、$、* 四個符號的解析,其它符號可以自行參考完善。
創建一個 MessageStraceAnalysis`.cs ,其代碼如下:
using System; using System.Collections.Generic; using System.IO; using System.Text; namespace CZGL.RedisClient { /// <summary> /// RESP 解析數據流 /// </summary> public class MessageStraceAnalysis<T> : MessageStrace { public MessageStraceAnalysis() { } /// <summary> /// 解析協議 /// </summary> /// <param name="data"></param> public override void Receive(MemoryStream stream, int length) { byte firstChar = (byte)stream.ReadByte(); // 首位字符,由於遊標已經到 1,所以後面 .GetBuffer(),都是從1開始截斷,首位字符舍棄; if (firstChar.CompareTo(RedisValueType.SimpleStrings) == 0) // 簡單字符串 { SetValue(Encoding.UTF8.GetString(stream.GetBuffer())); return; } else if (firstChar.CompareTo(RedisValueType.Errors) == 0) { TaskCompletionSource.SetException(new InvalidOperationException(Encoding.UTF8.GetString(stream.GetBuffer()))); return; } // 不是 + 和 - 開頭 stream.Position = 0; int index = 0; ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(stream.GetBuffer()); string tmp = Analysis(data, ref index); SetValue(tmp); } // 進入遞歸處理流程 private string Analysis(ReadOnlySpan<byte> data, ref int index) { // * if (data[index].CompareTo(RedisValueType.Arrays) == 0) { string value = default; index++; int size = BulkStrings(data, ref index); if (size == 0) return string.Empty; else if (size == -1) return null; for (int i = 0; i < size; i++) { var tmp = Analysis(data, ref index); value += tmp + ((i < (size - 1)) ? "\r\n" : string.Empty); } return value; } // $.. else if (data[index].CompareTo(RedisValueType.BulkStrings) == 0) { index++; int size = BulkStrings(data, ref index); if (size == 0) return string.Empty; else if (size == -1) return null; var value = Encoding.UTF8.GetString(data.Slice(index, size).ToArray()); index += size + 2; // 脫離之前,將指針移動到 \n 後 return value; } throw new ArgumentException("解析錯誤"); } } }
6,實現命令發送客戶端
由於 Redis 命令太多,如果直接將所有命令封裝到 RedisClient 中,必定使得 API 過的,而且代碼難以維護。因此,我們可以拆分,根據 string、hash、set 等 redis 類型,來設計客戶端。
下面來設計一個 StringClient:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CZGL.RedisClient { /// <summary> /// 字符串類型 /// </summary> public class StringClient : CommandClient<StringClient> { internal StringClient() { } internal StringClient(RedisClient client) : base(client) { } /// <summary> /// 設置鍵值 /// </summary> /// <param name="key">key</param> /// <param name="value">value</param> /// <returns></returns> public async Task<bool> Set(string key, string value) { await SendCommand<MessageStraceAnalysis<string>>($"{StringCommand.SET} {key} {value}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } /// <summary> /// 獲取一個鍵的值 /// </summary> /// <param name="key">鍵</param> /// <returns></returns> public async Task<string> Get(string key) { await SendCommand($"{StringCommand.GET} {key}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return result; } /// <summary> /// 從指定鍵的值中截取指定長度的數據 /// </summary> /// <param name="key">key</param> /// <param name="start">開始下標</param> /// <param name="end">結束下標</param> /// <returns></returns> public async Task<string> GetRance(string key, uint start, int end) { await SendCommand($"{StringCommand.GETRANGE} {key} {start} {end}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return result; } /// <summary> /// 設置一個值並返回舊的值 /// </summary> /// <param name="key"></param> /// <param name="newValue"></param> /// <returns></returns> public async Task<string> GetSet(string key, string newValue) { await SendCommand($"{StringCommand.GETSET} {key} {newValue}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return result; } /// <summary> /// 獲取二進制數據中某一位的值 /// </summary> /// <param name="key"></param> /// <param name="index"></param> /// <returns>0 或 1</returns> public async Task<int> GetBit(string key, uint index) { await SendCommand($"{StringCommand.GETBIT} {key} {index}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return Convert.ToInt32(result); } /// <summary> /// 設置某一位為 1 或 0 /// </summary> /// <param name="key"></param> /// <param name="index"></param> /// <param name="value">0或1</param> /// <returns></returns> public async Task<bool> SetBit(string key, uint index, uint value) { await SendCommand($"{StringCommand.SETBIT} {key} {index} {value}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } /// <summary> /// 獲取多個鍵的值 /// </summary> /// <param name="key"></param> /// <returns></returns> public async Task<string[]> MGet(params string[] key) { await SendCommand($"{StringCommand.MGET} {string.Join(" ", key)}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return result.Split("\r\n"); } private static class StringCommand { public const string SET = "SET"; public const string GET = "GET"; public const string GETRANGE = "GETRANGE"; public const string GETSET = "GETSET"; public const string GETBIT = "GETBIT"; public const string SETBIT = "SETBIT"; public const string MGET = "MGET"; // ... ... 更多 字符串的命令 } } }
StringClient 實現瞭 7個 Redis String 類型的命令,其它命令觸類旁通。
我們打開 RedisClient.cs,解除以下部分代碼的註釋:
private readonly Lazy<StringClient> stringClient; // 24 行 stringClient = new Lazy<StringClient>(() => new StringClient(this)); // 38 行 // 146 行 /// <summary> /// 獲取字符串請求客戶端 /// </summary> /// <returns></returns> public StringClient GetStringClient() { return stringClient.Value; }
7,如何使用
RedisClient 使用示例:
static async Task Main(string[] args) { RedisClient client = new RedisClient("127.0.0.1", 6379); var a = await client.ConnectAsync(); if (!a) { Console.WriteLine("連接服務器失敗"); Console.ReadKey(); return; } Console.WriteLine("連接服務器成功"); var stringClient = client.GetStringClient(); var result = await stringClient.Set("a", "123456789"); Console.Read(); }
封裝的消息命令支持異步。
8,更多客戶端
光 String 類型不過癮,我們繼續封裝更多的客戶端。
哈希:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CZGL.RedisClient { public class HashClient : CommandClient<HashClient> { internal HashClient(RedisClient client) : base(client) { } /// <summary> /// 設置哈希 /// </summary> /// <param name="key">鍵</param> /// <param name="values">字段-值列表</param> /// <returns></returns> public async Task<bool> HmSet(string key, Dictionary<string, string> values) { await SendCommand($"{StringCommand.HMSET} {key} {string.Join(" ", values.Select(x => $"{x.Key} {x.Value}").ToArray())})", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } public async Task<bool> HmSet<T>(string key, T values) { Dictionary<string, string> dic = new Dictionary<string, string>(); foreach (var item in typeof(T).GetProperties()) { dic.Add(item.Name, (string)item.GetValue(values)); } await SendCommand($"{StringCommand.HMSET} {key} {string.Join(" ", dic.Select(x => $"{x.Key} {x.Value}").ToArray())})", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } public async Task<object> HmGet(string key, string field) { await SendCommand($"{StringCommand.HMGET} {key} {field}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } private static class StringCommand { public const string HMSET = "HMSET "; public const string HMGET = "HMGET"; // ... ... 更多 字符串的命令 } } }
列表:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace CZGL.RedisClient { public class ListClient : CommandClient<ListClient> { internal ListClient(RedisClient client) : base(client) { } /// <summary> /// 設置鍵值 /// </summary> /// <param name="key">key</param> /// <param name="value">value</param> /// <returns></returns> public async Task<bool> LPush(string key, string value) { await SendCommand($"{StringCommand.LPUSH} {key} {value}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } public async Task<string> LRange(string key, int start, int end) { await SendCommand($"{StringCommand.LRANGE} {key} {start} {end}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return result; } private static class StringCommand { public const string LPUSH = "LPUSH"; public const string LRANGE = "LRANGE"; // ... ... 更多 字符串的命令 } } }
集合:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace CZGL.RedisClient { public class SetClient : CommandClient<SetClient> { internal SetClient() { } internal SetClient(RedisClient client) : base(client) { } public async Task<bool> SAdd(string key, string value) { await SendCommand($"{StringCommand.SADD} {key} {value}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } public async Task<string> SMembers(string key) { await SendCommand($"{StringCommand.SMEMBERS} {key}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return result; } private static class StringCommand { public const string SADD = "SADD"; public const string SMEMBERS = "SMEMBERS"; // ... ... 更多 字符串的命令 } } }
有序集合:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace CZGL.RedisClient { public class SortedClient : CommandClient<SortedClient> { internal SortedClient(RedisClient client) : base(client) { } public async Task<bool> ZAdd(string key, string value) { await SendCommand($"{StringCommand.ZADD} {key} {value}", out MessageStraceAnalysis<string> strace); var result = await strace.Task; return IsOk(result); } private static class StringCommand { public const string ZADD = "ZADD"; public const string SMEMBERS = "SMEMBERS"; // ... ... 更多 字符串的命令 } } }
這樣,我們就有一個具有簡單功能的 RedisClient 框架瞭。
9,更多測試
為瞭驗證功能是否可用,我們寫一些示例:
static RedisClient client = new RedisClient("127.0.0.1", 6379); static async Task Main(string[] args) { var a = await client.ConnectAsync(); if (!a) { Console.WriteLine("連接服務器失敗"); Console.ReadKey(); return; } Console.WriteLine("連接服務器成功"); await StringSETGET(); await StringGETRANGE(); await StringGETSET(); await StringMGet(); Console.ReadKey(); } static async Task StringSETGET() { var stringClient = client.GetStringClient(); var b = await stringClient.Set("seta", "6666"); var c = await stringClient.Get("seta"); if (c == "6666") { Console.WriteLine("true"); } } static async Task StringGETRANGE() { var stringClient = client.GetStringClient(); var b = await stringClient.Set("getrance", "123456789"); var c = await stringClient.GetRance("getrance", 0, -1); if (c == "123456789") { Console.WriteLine("true"); } var d = await stringClient.GetRance("getrance", 0, 3); if (d == "1234") { Console.WriteLine("true"); } } static async Task StringGETSET() { var stringClient = client.GetStringClient(); var b = await stringClient.Set("getrance", "123456789"); var c = await stringClient.GetSet("getrance", "987654321"); if (c == "123456789") { Console.WriteLine("true"); } } static async Task StringMGet() { var stringClient = client.GetStringClient(); var a = await stringClient.Set("stra", "123456789"); var b = await stringClient.Set("strb", "123456789"); var c = await stringClient.Set("strc", "123456789"); var d = await stringClient.MGet("stra", "strb", "strc"); if (d.Where(x => x == "123456789").Count() == 3) { Console.WriteLine("true"); } }
10,性能測試
因為隻是寫得比較簡單,而且是單線程,並且內存比較浪費,我覺得性能會比較差。但真相如何呢?我們來測試一下:
static RedisClient client = new RedisClient("127.0.0.1", 6379); static async Task Main(string[] args) { var a = await client.ConnectAsync(); if (!a) { Console.WriteLine("連接服務器失敗"); Console.ReadKey(); return; } Console.WriteLine("連接服務器成功"); var stringClient = client.GetStringClient(); Stopwatch watch = new Stopwatch(); watch.Start(); for (int i = 0; i < 3000; i++) { var guid = Guid.NewGuid().ToString(); _ = await stringClient.Set(guid, guid); _ = await stringClient.Get(guid); } watch.Stop(); Console.WriteLine($"總共耗時:{watch.ElapsedMilliseconds} ms"); Console.ReadKey(); }
耗時:
總共耗時:1003 ms
大概就是 1s,3000 個 SET 和 3000 個 GET 共 6000 個請求。看來單線程性能也是很強的。
本文教程源碼 Github 地址:https://github.com/whuanle/RedisClientLearn
以上所述是小編給大傢介紹的.NET Core實現簡單的Redis Client框架,希望對大傢有所幫助。在此也非常感謝大傢對WalkonNet網站的支持!