最近葉老闆寫了個 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 入門
11,關於 NCC
.NET Core Community (.NET 中心社群,簡稱 NCC)是一個基於並圍繞著 .NET 技術棧展開組織和活動的非官方、非盈利性的民間開源社群。我們希望通過我們 NCC 社群的努力,與各個開源社群一道為 .NET 生態注入更多活力。
加入 NCC,裡面一大把框架作者,教你寫框架,參與開源專案,做出你的貢獻。記得加入 NCC 喲~