最近叶老板写了个 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

1,关于 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 ,然后点击控制台,输入:

  1. set a 12
  2. set b 12
  3. set c 12
  4. MGET abc

以上命令每行按一下回车键。MGET 是 Redis 中一次性取出多个键的值的命令。

输出结果如下:

  1. 本地:0>SET a 12
  2. "OK"
  3. 本地:0>SET b 12
  4. "OK"
  5. 本地:0>SET c 12
  6. "OK"
  7. 本地:0>MGET a b c
  8. 1) "12"
  9. 2) "12"
  10. 3) "12"

但是这个管理工具以及去掉了 RESP 中的协议标识符,我们来写一个 demo 代码,还原 RESP 的本质。

  1. using System;
  2. using System.Linq;
  3. using System.Net;
  4. using System.Net.Sockets;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace ConsoleApp
  9. {
  10. class Program
  11. {
  12. static async Task Main(string[] args)
  13. {
  14. IPAddress IP = IPAddress.Parse("127.0.0.1");
  15. IPEndPoint IPEndPoint = new IPEndPoint(IP, 6379);
  16. Socket client = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  17. await client.ConnectAsync(IPEndPoint);
  18. if (!client.Connected)
  19. {
  20. Console.WriteLine("连接 Redis 服务器失败!");
  21. Console.Read();
  22. }
  23. Console.WriteLine("恭喜恭喜,连接 Redis 服务器成功");
  24. // 后台接收消息
  25. new Thread(() =>
  26. {
  27. while (true)
  28. {
  29. byte[] data = new byte[100];
  30. int size = client.Receive(data);
  31. Console.WriteLine();
  32. Console.WriteLine(Encoding.UTF8.GetString(data));
  33. Console.WriteLine();
  34. }
  35. }).Start();
  36. while (true)
  37. {
  38. Console.Write("$> ");
  39. string command = Console.ReadLine();
  40. // 发送的命令必须以 \r\n 结尾
  41. int size = client.Send(Encoding.UTF8.GetBytes(command + "\r\n"));
  42. Thread.Sleep(100);
  43. }
  44. }
  45. }
  46. }

输入以及输出结果:

  1. $> SET a 123456789
  2. +OK
  3. $> SET b 123456789
  4. +OK
  5. $> SET c 123456789
  6. +OK
  7. $> MGET a b c
  8. *3
  9. $9
  10. 123456789
  11. $9
  12. 123456789
  13. $9
  14. 123456789

可见,Redis 响应的消息内容,是以 $、*、+ 等字符开头的,并且使用 \r\n 分隔。

我们写 Redis Client 的方法就是接收 socket 内容,然后从中解析出实际的数据。

每次发送设置命令成功,都会返回 +OK;*3 表示有三个数组;$9 表示接收的数据长度是 9;

大概就是这样了,下面我们来写一个简单的 Redis Client 框架,然后睡觉。

记得使用 netstandard2.1,因为有些 byte[] 、string、ReadOnlySpan<T> 的转换,需要 netstandard2.1 才能更加方便。

定义数据类型

根据前面的 demo,我们来定义一个类型,存储那些特殊符号:

  1. /// <summary>
  2. /// RESP Response 类型
  3. /// </summary>
  4. public static class RedisValueType
  5. {
  6. public const byte Errors = (byte)'-';
  7. public const byte SimpleStrings = (byte)'+';
  8. public const byte Integers = (byte)':';
  9. public const byte BulkStrings = (byte)'$';
  10. public const byte Arrays = (byte)'*';
  11. public const byte R = (byte)'\r';
  12. public const byte N = (byte)'\n';
  13. }

2,定义异步消息状态机

创建一个 MessageStrace 类,作用是作为消息响应的异步状态机,并且具有解析数据流的功能。

  1. /// <summary>
  2. /// 自定义消息队列状态机
  3. /// </summary>
  4. public abstract class MessageStrace
  5. {
  6. protected MessageStrace()
  7. {
  8. TaskCompletionSource = new TaskCompletionSource<string>();
  9. Task = TaskCompletionSource.Task;
  10. }
  11. protected readonly TaskCompletionSource<string> TaskCompletionSource;
  12. /// <summary>
  13. /// 标志任务是否完成,并接收 redis 响应的字符串数据流
  14. /// </summary>
  15. public Task<string> Task { get; private set; }
  16. /// <summary>
  17. /// 接收数据流
  18. /// </summary>
  19. /// <param name="stream"></param>
  20. /// <param name="length">实际长度</param>
  21. public abstract void Receive(MemoryStream stream, int length);
  22. /// <summary>
  23. /// 响应已经完成
  24. /// </summary>
  25. /// <param name="data"></param>
  26. protected void SetValue(string data)
  27. {
  28. TaskCompletionSource.SetResult(data);
  29. }
  30. /// <summary>
  31. /// 解析 $ 或 * 符号后的数字,必须传递符后后一位的下标
  32. /// </summary>
  33. /// <param name="data"></param>
  34. /// <param name="index">解析到的位置</param>
  35. /// <returns></returns>
  36. protected int BulkStrings(ReadOnlySpan<byte> data, ref int index)
  37. {
  38. int start = index;
  39. int end = start;
  40. while (true)
  41. {
  42. if (index + 1 >= data.Length)
  43. throw new ArgumentOutOfRangeException("溢出");
  44. // \r\n
  45. if (data[index].CompareTo(RedisValueType.R) == 0 && data[index + 1].CompareTo(RedisValueType.N) == 0)
  46. {
  47. index += 2; // 指向 \n 的下一位
  48. break;
  49. }
  50. end++;
  51. index++;
  52. }
  53. // 截取 $2 *3 符号后面的数字
  54. return Convert.ToInt32(Encoding.UTF8.GetString(data.Slice(start, end - start).ToArray()));
  55. }
  56. }

3,定义命令发送模板

由于 Redis 命令非常多,为了更加好的封装,我们定义一个消息发送模板,规定五种类型分别使用五种类型发送 Client。

定义一个统一的模板类:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Threading.Tasks;
  5. namespace CZGL.RedisClient
  6. {
  7. /// <summary>
  8. /// 命令发送模板
  9. /// </summary>
  10. public abstract class CommandClient<T> where T : CommandClient<T>
  11. {
  12. protected RedisClient _client;
  13. protected CommandClient()
  14. {
  15. }
  16. protected CommandClient(RedisClient client)
  17. {
  18. _client = client;
  19. }
  20. /// <summary>
  21. /// 复用
  22. /// </summary>
  23. /// <param name="client"></param>
  24. /// <returns></returns>
  25. internal virtual CommandClient<T> Init(RedisClient client)
  26. {
  27. _client = client;
  28. return this;
  29. }
  30. /// <summary>
  31. /// 请求是否成功
  32. /// </summary>
  33. /// <param name="value">响应的消息</param>
  34. /// <returns></returns>
  35. protected bool IsOk(string value)
  36. {
  37. if (value[0].CompareTo('+') != 0 || value[1].CompareTo('O') != 0 || value[2].CompareTo('K') != 0)
  38. return false;
  39. return true;
  40. }
  41. /// <summary>
  42. /// 发送命令
  43. /// </summary>
  44. /// <param name="command">发送的命令</param>
  45. /// <param name="strace">数据类型客户端</param>
  46. /// <returns></returns>
  47. protected Task SendCommand<TStrace>(string command, out TStrace strace) where TStrace : MessageStrace, new()
  48. {
  49. strace = new TStrace();
  50. return _client.SendAsync(strace, command);
  51. }
  52. }
  53. }

4,定义 Redis Client

RedisClient 类用于发送 Redis 命令,然后将任务放到队列中;接收 Redis 返回的数据内容,并将数据流写入内存中,调出队列,设置异步任务的返回值。

Send 过程可以并发,但是接收消息内容使用单线程。为了保证消息的顺序性,采用队列来记录 Send - Receive 的顺序。

C# 的 Socket 比较操蛋,想搞并发和高性能 Socket 不是那么容易。

以下代码有三个地方注释了,后面继续编写其它代码会用到。

  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Net;
  6. using System.Net.Sockets;
  7. using System.Runtime.CompilerServices;
  8. using System.Text;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. namespace CZGL.RedisClient
  12. {
  13. /// <summary>
  14. /// Redis 客户端
  15. /// </summary>
  16. public class RedisClient
  17. {
  18. private readonly IPAddress IP;
  19. private readonly IPEndPoint IPEndPoint;
  20. private readonly Socket client;
  21. //private readonly Lazy<StringClient> stringClient;
  22. //private readonly Lazy<HashClient> hashClient;
  23. //private readonly Lazy<ListClient> listClient;
  24. //private readonly Lazy<SetClient> setClient;
  25. //private readonly Lazy<SortedClient> sortedClient;
  26. // 数据流请求队列
  27. private readonly ConcurrentQueue<MessageStrace> StringTaskQueue = new ConcurrentQueue<MessageStrace>();
  28. public RedisClient(string ip, int port)
  29. {
  30. IP = IPAddress.Parse(ip);
  31. IPEndPoint = new IPEndPoint(IP, port);
  32. //stringClient = new Lazy<StringClient>(() => new StringClient(this));
  33. //hashClient = new Lazy<HashClient>(() => new HashClient(this));
  34. //listClient = new Lazy<ListClient>(() => new ListClient(this));
  35. //setClient = new Lazy<SetClient>(() => new SetClient(this));
  36. //sortedClient = new Lazy<SortedClient>(() => new SortedClient(this));
  37. client = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  38. }
  39. /// <summary>
  40. /// 开始连接 Redis
  41. /// </summary>
  42. public async Task<bool> ConnectAsync()
  43. {
  44. await client.ConnectAsync(IPEndPoint);
  45. new Thread(() => { ReceiveQueue(); })
  46. {
  47. IsBackground = true
  48. }.Start();
  49. return client.Connected;
  50. }
  51. /// <summary>
  52. /// 发送一个命令,将其加入队列
  53. /// </summary>
  54. /// <param name="task"></param>
  55. /// <param name="command"></param>
  56. /// <returns></returns>
  57. internal Task<int> SendAsync(MessageStrace task, string command)
  58. {
  59. var buffer = Encoding.UTF8.GetBytes(command + "\r\n");
  60. var result = client.SendAsync(new ArraySegment<byte>(buffer, 0, buffer.Length), SocketFlags.None);
  61. StringTaskQueue.Enqueue(task);
  62. return result;
  63. }
  64. /*
  65. Microsoft 对缓冲区输入不同大小的数据,测试响应时间。
  66. 1024 - real 0m0,102s; user 0m0,018s; sys 0m0,009s
  67. 2048 - real 0m0,112s; user 0m0,017s; sys 0m0,009s
  68. 8192 - real 0m0,163s; user 0m0,017s; sys 0m0,007s
  69. 256 - real 0m0,101s; user 0m0,019s; sys 0m0,008s
  70. 16 - real 0m0,144s; user 0m0,016s; sys 0m0,010s
  71. .NET Socket,默认缓冲区的大小为 8192 字节。
  72. Socket.ReceiveBufferSize: An Int32 that contains the size, in bytes, of the receive buffer. The default is 8192.
  73. 但响应中有很多只是 "+OK\r\n" 这样的响应,并且 MemoryStream 刚好默认是 256(当然,可以自己设置大小),缓冲区过大,浪费内存;
  74. 超过 256 这个大小,MemoryStream 会继续分配新的 256 大小的内存区域,会消耗性能。
  75. BufferSize 设置为 256 ,是比较合适的做法。
  76. */
  77. private const int BufferSize = 256;
  78. /// <summary>
  79. /// 单线程串行接收数据流,调出任务队列完成任务
  80. /// </summary>
  81. private void ReceiveQueue()
  82. {
  83. while (true)
  84. {
  85. MemoryStream stream = new MemoryStream(BufferSize); // 内存缓存区
  86. byte[] data = new byte[BufferSize]; // 分片,每次接收 N 个字节
  87. int size = client.Receive(data); // 等待接收一个消息
  88. int length = size; // 数据流总长度
  89. while (true)
  90. {
  91. stream.Write(data, 0, size); // 分片接收的数据流写入内存缓冲区
  92. // 数据流接收完毕
  93. if (size < BufferSize) // 存在 Bug ,当数据流的大小或者数据流分片最后一片的字节大小刚刚好为 BufferSize 大小时,无法跳出 Receive
  94. {
  95. break;
  96. }
  97. length += client.Receive(data); // 还没有接收完毕,继续接收
  98. }
  99. stream.Seek(0, SeekOrigin.Begin); // 重置游标位置
  100. // 调出队列
  101. StringTaskQueue.TryDequeue(out var tmpResult);
  102. // 处理队列中的任务
  103. tmpResult.Receive(stream, length);
  104. }
  105. }
  106. /// <summary>
  107. /// 复用
  108. /// </summary>
  109. /// <typeparam name="T"></typeparam>
  110. /// <param name="client"></param>
  111. /// <returns></returns>
  112. public T GetClient<T>(T client) where T : CommandClient<T>
  113. {
  114. client.Init(this);
  115. return client;
  116. }
  117. ///// <summary>
  118. ///// 获取字符串请求客户端
  119. ///// </summary>
  120. ///// <returns></returns>
  121. //public StringClient GetStringClient()
  122. //{
  123. // return stringClient.Value;
  124. //}
  125. //public HashClient GetHashClient()
  126. //{
  127. // return hashClient.Value;
  128. //}
  129. //public ListClient GetListClient()
  130. //{
  131. // return listClient.Value;
  132. //}
  133. //public SetClient GetSetClient()
  134. //{
  135. // return setClient.Value;
  136. //}
  137. //public SortedClient GetSortedClient()
  138. //{
  139. // return sortedClient.Value;
  140. //}
  141. }
  142. }

5,实现简单的 RESP 解析

下面使用代码来实现对 Redis RESP 消息的解析,时间问题,我只实现 +、-、$、* 四个符号的解析,其它符号可以自行参考完善。

创建一个 MessageStraceAnalysis`.cs ,其代码如下:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. namespace CZGL.RedisClient
  6. {
  7. /// <summary>
  8. /// RESP 解析数据流
  9. /// </summary>
  10. public class MessageStraceAnalysis<T> : MessageStrace
  11. {
  12. public MessageStraceAnalysis()
  13. {
  14. }
  15. /// <summary>
  16. /// 解析协议
  17. /// </summary>
  18. /// <param name="data"></param>
  19. public override void Receive(MemoryStream stream, int length)
  20. {
  21. byte firstChar = (byte)stream.ReadByte(); // 首位字符,由于游标已经到 1,所以后面 .GetBuffer(),都是从1开始截断,首位字符舍弃;
  22. if (firstChar.CompareTo(RedisValueType.SimpleStrings) == 0) // 简单字符串
  23. {
  24. SetValue(Encoding.UTF8.GetString(stream.GetBuffer()));
  25. return;
  26. }
  27. else if (firstChar.CompareTo(RedisValueType.Errors) == 0)
  28. {
  29. TaskCompletionSource.SetException(new InvalidOperationException(Encoding.UTF8.GetString(stream.GetBuffer())));
  30. return;
  31. }
  32. // 不是 + 和 - 开头
  33. stream.Position = 0;
  34. int index = 0;
  35. ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(stream.GetBuffer());
  36. string tmp = Analysis(data, ref index);
  37. SetValue(tmp);
  38. }
  39. // 进入递归处理流程
  40. private string Analysis(ReadOnlySpan<byte> data, ref int index)
  41. {
  42. // *
  43. if (data[index].CompareTo(RedisValueType.Arrays) == 0)
  44. {
  45. string value = default;
  46. index++;
  47. int size = BulkStrings(data, ref index);
  48. if (size == 0)
  49. return string.Empty;
  50. else if (size == -1)
  51. return null;
  52. for (int i = 0; i < size; i++)
  53. {
  54. var tmp = Analysis(data, ref index);
  55. value += tmp + ((i < (size - 1)) ? "\r\n" : string.Empty);
  56. }
  57. return value;
  58. }
  59. // $..
  60. else if (data[index].CompareTo(RedisValueType.BulkStrings) == 0)
  61. {
  62. index++;
  63. int size = BulkStrings(data, ref index);
  64. if (size == 0)
  65. return string.Empty;
  66. else if (size == -1)
  67. return null;
  68. var value = Encoding.UTF8.GetString(data.Slice(index, size).ToArray());
  69. index += size + 2; // 脱离之前,将指针移动到 \n 后
  70. return value;
  71. }
  72. throw new ArgumentException("解析错误");
  73. }
  74. }
  75. }

6,实现命令发送客户端

由于 Redis 命令太多,如果直接将所有命令封装到 RedisClient 中,必定使得 API 过的,而且代码难以维护。因此,我们可以拆分,根据 string、hash、set 等 redis 类型,来设计客户端。

下面来设计一个 StringClient:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace CZGL.RedisClient
  7. {
  8. /// <summary>
  9. /// 字符串类型
  10. /// </summary>
  11. public class StringClient : CommandClient<StringClient>
  12. {
  13. internal StringClient()
  14. {
  15. }
  16. internal StringClient(RedisClient client) : base(client)
  17. {
  18. }
  19. /// <summary>
  20. /// 设置键值
  21. /// </summary>
  22. /// <param name="key">key</param>
  23. /// <param name="value">value</param>
  24. /// <returns></returns>
  25. public async Task<bool> Set(string key, string value)
  26. {
  27. await SendCommand<MessageStraceAnalysis<string>>($"{StringCommand.SET} {key} {value}", out MessageStraceAnalysis<string> strace);
  28. var result = await strace.Task;
  29. return IsOk(result);
  30. }
  31. /// <summary>
  32. /// 获取一个键的值
  33. /// </summary>
  34. /// <param name="key">键</param>
  35. /// <returns></returns>
  36. public async Task<string> Get(string key)
  37. {
  38. await SendCommand($"{StringCommand.GET} {key}", out MessageStraceAnalysis<string> strace);
  39. var result = await strace.Task;
  40. return result;
  41. }
  42. /// <summary>
  43. /// 从指定键的值中截取指定长度的数据
  44. /// </summary>
  45. /// <param name="key">key</param>
  46. /// <param name="start">开始下标</param>
  47. /// <param name="end">结束下标</param>
  48. /// <returns></returns>
  49. public async Task<string> GetRance(string key, uint start, int end)
  50. {
  51. await SendCommand($"{StringCommand.GETRANGE} {key} {start} {end}", out MessageStraceAnalysis<string> strace);
  52. var result = await strace.Task;
  53. return result;
  54. }
  55. /// <summary>
  56. /// 设置一个值并返回旧的值
  57. /// </summary>
  58. /// <param name="key"></param>
  59. /// <param name="newValue"></param>
  60. /// <returns></returns>
  61. public async Task<string> GetSet(string key, string newValue)
  62. {
  63. await SendCommand($"{StringCommand.GETSET} {key} {newValue}", out MessageStraceAnalysis<string> strace);
  64. var result = await strace.Task;
  65. return result;
  66. }
  67. /// <summary>
  68. /// 获取二进制数据中某一位的值
  69. /// </summary>
  70. /// <param name="key"></param>
  71. /// <param name="index"></param>
  72. /// <returns>0 或 1</returns>
  73. public async Task<int> GetBit(string key, uint index)
  74. {
  75. await SendCommand($"{StringCommand.GETBIT} {key} {index}", out MessageStraceAnalysis<string> strace);
  76. var result = await strace.Task;
  77. return Convert.ToInt32(result);
  78. }
  79. /// <summary>
  80. /// 设置某一位为 1 或 0
  81. /// </summary>
  82. /// <param name="key"></param>
  83. /// <param name="index"></param>
  84. /// <param name="value">0或1</param>
  85. /// <returns></returns>
  86. public async Task<bool> SetBit(string key, uint index, uint value)
  87. {
  88. await SendCommand($"{StringCommand.SETBIT} {key} {index} {value}", out MessageStraceAnalysis<string> strace);
  89. var result = await strace.Task;
  90. return IsOk(result);
  91. }
  92. /// <summary>
  93. /// 获取多个键的值
  94. /// </summary>
  95. /// <param name="key"></param>
  96. /// <returns></returns>
  97. public async Task<string[]> MGet(params string[] key)
  98. {
  99. await SendCommand($"{StringCommand.MGET} {string.Join(" ", key)}", out MessageStraceAnalysis<string> strace);
  100. var result = await strace.Task;
  101. return result.Split("\r\n");
  102. }
  103. private static class StringCommand
  104. {
  105. public const string SET = "SET";
  106. public const string GET = "GET";
  107. public const string GETRANGE = "GETRANGE";
  108. public const string GETSET = "GETSET";
  109. public const string GETBIT = "GETBIT";
  110. public const string SETBIT = "SETBIT";
  111. public const string MGET = "MGET";
  112. // ... ... 更多 字符串的命令
  113. }
  114. }
  115. }

StringClient 实现了 7个 Redis String 类型的命令,其它命令触类旁通。

我们打开 RedisClient.cs,解除以下部分代码的注释:

  1. private readonly Lazy<StringClient> stringClient; // 24 行
  2. stringClient = new Lazy<StringClient>(() => new StringClient(this)); // 38 行
  3. // 146 行
  4. /// <summary>
  5. /// 获取字符串请求客户端
  6. /// </summary>
  7. /// <returns></returns>
  8. public StringClient GetStringClient()
  9. {
  10. return stringClient.Value;
  11. }

7,如何使用

RedisClient 使用示例:

  1. static async Task Main(string[] args)
  2. {
  3. RedisClient client = new RedisClient("127.0.0.1", 6379);
  4. var a = await client.ConnectAsync();
  5. if (!a)
  6. {
  7. Console.WriteLine("连接服务器失败");
  8. Console.ReadKey();
  9. return;
  10. }
  11. Console.WriteLine("连接服务器成功");
  12. var stringClient = client.GetStringClient();
  13. var result = await stringClient.Set("a", "123456789");
  14. Console.Read();
  15. }

封装的消息命令支持异步。

8,更多客户端

光 String 类型不过瘾,我们继续封装更多的客户端。

哈希:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace CZGL.RedisClient
  7. {
  8. public class HashClient : CommandClient<HashClient>
  9. {
  10. internal HashClient(RedisClient client) : base(client)
  11. {
  12. }
  13. /// <summary>
  14. /// 设置哈希
  15. /// </summary>
  16. /// <param name="key">键</param>
  17. /// <param name="values">字段-值列表</param>
  18. /// <returns></returns>
  19. public async Task<bool> HmSet(string key, Dictionary<string, string> values)
  20. {
  21. await SendCommand($"{StringCommand.HMSET} {key} {string.Join(" ", values.Select(x => $"{x.Key} {x.Value}").ToArray())})", out MessageStraceAnalysis<string> strace);
  22. var result = await strace.Task;
  23. return IsOk(result);
  24. }
  25. public async Task<bool> HmSet<T>(string key, T values)
  26. {
  27. Dictionary<string, string> dic = new Dictionary<string, string>();
  28. foreach (var item in typeof(T).GetProperties())
  29. {
  30. dic.Add(item.Name, (string)item.GetValue(values));
  31. }
  32. await SendCommand($"{StringCommand.HMSET} {key} {string.Join(" ", dic.Select(x => $"{x.Key} {x.Value}").ToArray())})", out MessageStraceAnalysis<string> strace);
  33. var result = await strace.Task;
  34. return IsOk(result);
  35. }
  36. public async Task<object> HmGet(string key, string field)
  37. {
  38. await SendCommand($"{StringCommand.HMGET} {key} {field}", out MessageStraceAnalysis<string> strace);
  39. var result = await strace.Task;
  40. return IsOk(result);
  41. }
  42. private static class StringCommand
  43. {
  44. public const string HMSET = "HMSET ";
  45. public const string HMGET = "HMGET";
  46. // ... ... 更多 字符串的命令
  47. }
  48. }
  49. }

列表:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Threading.Tasks;
  5. namespace CZGL.RedisClient
  6. {
  7. public class ListClient : CommandClient<ListClient>
  8. {
  9. internal ListClient(RedisClient client) : base(client)
  10. {
  11. }
  12. /// <summary>
  13. /// 设置键值
  14. /// </summary>
  15. /// <param name="key">key</param>
  16. /// <param name="value">value</param>
  17. /// <returns></returns>
  18. public async Task<bool> LPush(string key, string value)
  19. {
  20. await SendCommand($"{StringCommand.LPUSH} {key} {value}", out MessageStraceAnalysis<string> strace);
  21. var result = await strace.Task;
  22. return IsOk(result);
  23. }
  24. public async Task<string> LRange(string key, int start, int end)
  25. {
  26. await SendCommand($"{StringCommand.LRANGE} {key} {start} {end}", out MessageStraceAnalysis<string> strace);
  27. var result = await strace.Task;
  28. return result;
  29. }
  30. private static class StringCommand
  31. {
  32. public const string LPUSH = "LPUSH";
  33. public const string LRANGE = "LRANGE";
  34. // ... ... 更多 字符串的命令
  35. }
  36. }
  37. }

集合:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Threading.Tasks;
  5. namespace CZGL.RedisClient
  6. {
  7. public class SetClient : CommandClient<SetClient>
  8. {
  9. internal SetClient() { }
  10. internal SetClient(RedisClient client) : base(client)
  11. {
  12. }
  13. public async Task<bool> SAdd(string key, string value)
  14. {
  15. await SendCommand($"{StringCommand.SADD} {key} {value}", out MessageStraceAnalysis<string> strace);
  16. var result = await strace.Task;
  17. return IsOk(result);
  18. }
  19. public async Task<string> SMembers(string key)
  20. {
  21. await SendCommand($"{StringCommand.SMEMBERS} {key}", out MessageStraceAnalysis<string> strace);
  22. var result = await strace.Task;
  23. return result;
  24. }
  25. private static class StringCommand
  26. {
  27. public const string SADD = "SADD";
  28. public const string SMEMBERS = "SMEMBERS";
  29. // ... ... 更多 字符串的命令
  30. }
  31. }
  32. }

有序集合:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Threading.Tasks;
  5. namespace CZGL.RedisClient
  6. {
  7. public class SortedClient : CommandClient<SortedClient>
  8. {
  9. internal SortedClient(RedisClient client) : base(client)
  10. {
  11. }
  12. public async Task<bool> ZAdd(string key, string value)
  13. {
  14. await SendCommand($"{StringCommand.ZADD} {key} {value}", out MessageStraceAnalysis<string> strace);
  15. var result = await strace.Task;
  16. return IsOk(result);
  17. }
  18. private static class StringCommand
  19. {
  20. public const string ZADD = "ZADD";
  21. public const string SMEMBERS = "SMEMBERS";
  22. // ... ... 更多 字符串的命令
  23. }
  24. }
  25. }

这样,我们就有一个具有简单功能的 RedisClient 框架了。

9,更多测试

为了验证功能是否可用,我们写一些示例:

  1. static RedisClient client = new RedisClient("127.0.0.1", 6379);
  2. static async Task Main(string[] args)
  3. {
  4. var a = await client.ConnectAsync();
  5. if (!a)
  6. {
  7. Console.WriteLine("连接服务器失败");
  8. Console.ReadKey();
  9. return;
  10. }
  11. Console.WriteLine("连接服务器成功");
  12. await StringSETGET();
  13. await StringGETRANGE();
  14. await StringGETSET();
  15. await StringMGet();
  16. Console.ReadKey();
  17. }
  18. static async Task StringSETGET()
  19. {
  20. var stringClient = client.GetStringClient();
  21. var b = await stringClient.Set("seta", "6666");
  22. var c = await stringClient.Get("seta");
  23. if (c == "6666")
  24. {
  25. Console.WriteLine("true");
  26. }
  27. }
  28. static async Task StringGETRANGE()
  29. {
  30. var stringClient = client.GetStringClient();
  31. var b = await stringClient.Set("getrance", "123456789");
  32. var c = await stringClient.GetRance("getrance", 0, -1);
  33. if (c == "123456789")
  34. {
  35. Console.WriteLine("true");
  36. }
  37. var d = await stringClient.GetRance("getrance", 0, 3);
  38. if (d == "1234")
  39. {
  40. Console.WriteLine("true");
  41. }
  42. }
  43. static async Task StringGETSET()
  44. {
  45. var stringClient = client.GetStringClient();
  46. var b = await stringClient.Set("getrance", "123456789");
  47. var c = await stringClient.GetSet("getrance", "987654321");
  48. if (c == "123456789")
  49. {
  50. Console.WriteLine("true");
  51. }
  52. }
  53. static async Task StringMGet()
  54. {
  55. var stringClient = client.GetStringClient();
  56. var a = await stringClient.Set("stra", "123456789");
  57. var b = await stringClient.Set("strb", "123456789");
  58. var c = await stringClient.Set("strc", "123456789");
  59. var d = await stringClient.MGet("stra", "strb", "strc");
  60. if (d.Where(x => x == "123456789").Count() == 3)
  61. {
  62. Console.WriteLine("true");
  63. }
  64. }

10,性能测试

因为只是写得比较简单,而且是单线程,并且内存比较浪费,我觉得性能会比较差。但真相如何呢?我们来测试一下:

  1. static RedisClient client = new RedisClient("127.0.0.1", 6379);
  2. static async Task Main(string[] args)
  3. {
  4. var a = await client.ConnectAsync();
  5. if (!a)
  6. {
  7. Console.WriteLine("连接服务器失败");
  8. Console.ReadKey();
  9. return;
  10. }
  11. Console.WriteLine("连接服务器成功");
  12. var stringClient = client.GetStringClient();
  13. Stopwatch watch = new Stopwatch();
  14. watch.Start();
  15. for (int i = 0; i < 3000; i++)
  16. {
  17. var guid = Guid.NewGuid().ToString();
  18. _ = await stringClient.Set(guid, guid);
  19. _ = await stringClient.Get(guid);
  20. }
  21. watch.Stop();
  22. Console.WriteLine($"总共耗时:{watch.ElapsedMilliseconds/10} ms");
  23. Console.ReadKey();
  24. }

耗时:

  1. 总共耗时: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 哟~

教你写个简单到的 Redis Client 框架 - .NET Core的更多相关文章

  1. Jquery教你写一个简单的轮播.

    这个我表示写的不咋地-_-//,但是胜在简单,可优化性不错. 实际上我本来想写个复杂点的结构的,但是最近忙成狗了!!!!所以大家就讲究着看吧 HTML结构 <div class="ba ...

  2. JAVA RPC (六) 之手把手从零教你写一个生产级RPC之client的代理

    首先对于RPC来讲,最主要的无非三点[SERVER IO模型].[序列化协议].[client连接池复用],之前的博客大家应该对thrift有一个大致的了解了,那么我们现在来说一说如何将thrift的 ...

  3. JAVA RPC (七) 手把手从零教你写一个生产级RPC之client请求

    上节说了关于通用请求代理,实际上对spring的bean引用都是通过koalasClientProxy来实现的,那么在代理方法中才是我们实际的发送逻辑,咱们先看一下原生的thrift请求是什么样的. ...

  4. 手把手教你从零写一个简单的 VUE

    本系列是一个教程,下面贴下目录~1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 今天给大家带来的是实现一个简单的类似 VUE 一样的前端框架,VUE 框架现在应 ...

  5. 手把手教你从零写一个简单的 VUE--模板篇

    教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的数据驱动视图 ...

  6. [原创]手把手教你写网络爬虫(7):URL去重

    手把手教你写网络爬虫(7) 作者:拓海 摘要:从零开始写爬虫,初学者的速成指南! 封面: 本期我们来聊聊URL去重那些事儿.以前我们曾使用Python的字典来保存抓取过的URL,目的是将重复抓取的UR ...

  7. 手把手教你写Sublime中的Snippet

    手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...

  8. 手把手教你写电商爬虫-第三课 实战尚妆网AJAX请求处理和内容提取

    版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 看完两篇,相信大家已经从开始的 ...

  9. 手把手教你写电商爬虫-第四课 淘宝网商品爬虫自动JS渲染

    版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 手把手教你写电商爬虫-第三课 ...

随机推荐

  1. 【题解】[USACO13FEB]Tractor S

    题目戳我 \(\text{Solution:}\) 好久没写啥\(dfs\)了,借这个题整理下细节. 观察到答案具有二分性,所以先求出其差的最大最小值,\(\log val\)的复杂度不成问题. 考虑 ...

  2. Java数组以及内存分配

    Java数组以及内存分配 什么数组(简) 数组初始化 动态初始化 静态初始化 内存分配问题(重) 数组操作的两个常见小问题 什么是数组: 定义格式: 数组类型 [] 数组名 ; 如:常用格式,其他方式 ...

  3. Nginx(五)、http反向代理的实现

    上一篇nginx的文章中,我们理解了整个http正向代理的运行流程原理,主要就是事件机制接入,header解析,body解析,然后遍历各种checker,以及详细讲解了其正向代理的具体实现过程.这已经 ...

  4. JVM 第三篇:Java 类加载机制

    本文内容过于硬核,建议有 Java 相关经验人士阅读. 1. 什么是类的加载? 类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 ...

  5. 《流畅的Python》 第一部分 序章 【数据模型】

    流畅的Python 致Marta,用我全心全意的爱 第一部分 序幕 第一章 Python数据模型 特殊方法 定义: Python解释器碰到特殊句法时,使用特殊方法激活对象的基本操作,例如python语 ...

  6. golang 语言的特性

    给函数传递参数的时候 map.slice.channel是按引用传递的 同一个变量不能用 := 这种方式创建并赋值两次. 一个包(package)的func .结构体类型变量如果要被外部的包调用.fu ...

  7. MeteoInfoLab脚本示例:数据投影-FLEXPART

    FLEXPART是一个类似HYSPLIT的扩散模式,它输出的netcdf文件参照了WRF,可惜全局属性没有写全,比如只有一个投影名称(例如Lambert),没有相关的投影参数:中央经度,标准纬度等等. ...

  8. python 字典使用——增删改查

    创建字典 dict= {key1 : value1, key2 : value2 } key : value 为键值对 增: dict[key] = value 删: del dict[key] 改: ...

  9. 分布式锁结合SpringCache

    1.高并发缓存失效问题: 缓存穿透: 指查询一个一定不存在的数据,由于缓存不命中导致去查询数据库,但数据库也无此记录,我们没有将此次查询的null写入缓存,导致这个不存在的数据每次请求都要到存储层进行 ...

  10. day69:Vue:组件化开发&Vue-Router&Vue-client

    目录 组件化开发 1.什么是组件? 2.局部组件 3.全局组件 4.父组件向子组件传值 5.子组件往父组件传值 6.平行组件传值 Vue-Router的使用 Vue自动化工具:Vue-Client 组 ...