.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網站的支持!

推薦閱讀: