教你寫個簡單的 Redis Client 框架 - .NET Core

痴者工良發表於2020-11-10

最近葉老闆寫了個 FreeRedis,功能強悍,效能驚人,比子彈更快,比引擎更有力,剛好前段時間在學習 Redis,於是跟風試試也寫一個簡單的 RedisClient。

FreeRedis 專案地址:https://github.com/2881099/FreeRedis

本文教程原始碼 Github 地址:https://github.com/whuanle/RedisClientLearn

由於程式碼簡單,不考慮太多功能,不支援密碼登入;不支援叢集;不支援併發;

首先之行在自己電腦安裝 redis,Windows 版本下載地址:https://github.com/MicrosoftArchive/redis/releases

然後下載 Windows 版的 Redis 管理器

Windows 版本的 Redis Desktop Manager 64位 2019.1(中文版) 下載地址 https://www.7down.com/soft/233274.html

官方正版最新版本下載地址 https://redisdesktop.com/download

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

以上命令每行按一下Enter鍵。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/10} ms");
            Console.ReadKey();
        }

耗時:

總共耗時:1003 ms

大概就是 1s,3000 個 SET 和 3000 個 GET 共 6000 個請求。看來單執行緒效能也是很強的。

不知不覺快 11 點了,不寫了,趕緊睡覺去了。

筆者其它 Redis 文章:

搭建分散式 Redis Cluster 叢集與 Redis 入門

Redis 入門與 ASP.NET Core 快取

11,關於 NCC

.NET Core Community (.NET 中心社群,簡稱 NCC)是一個基於並圍繞著 .NET 技術棧展開組織和活動的非官方、非盈利性的民間開源社群。我們希望通過我們 NCC 社群的努力,與各個開源社群一道為 .NET 生態注入更多活力。

加入 NCC,裡面一大把框架作者,教你寫框架,參與開源專案,做出你的貢獻。記得加入 NCC 喲~

相關文章