.NET Core实现简单的Redis Client框架

目录
  • 0,关于RedisRESP
  • 1,定义数据类型
  • 2,定义异步消息状态机
  • 3,定义命令发送模板
  • 4,定义RedisClient
  • 5,实现简单的RESP解析
  • 6,实现命令发送客户端
  • 7,如何使用
  • 8,更多客户端
  • 9,更多测试
  • 10,性能测试

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框架,希望对大家有所帮助。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 详解Asp.net Core 使用Redis存储Session

    前言 Asp.net Core 改变了之前的封闭,现在开源且开放,下面我们来用Redis存储Session来做一个简单的测试,或者叫做中间件(middleware). 对于Session来说褒贬不一,很多人直接说不要用,也有很多人在用,这个也没有绝对的这义,个人认为只要不影什么且又可以方便实现的东西是可以用的,现在不对可不可用做表态,我们只关心实现. 类库引用 这个相对于之前的.net是方便了不少,需要在project.json中的dependencies节点中添加如下内容: "StackExc

  • .NET Core中使用Redis与Memcached的序列化问题详析

    前言 在使用分布式缓存的时候,都不可避免的要做这样一步操作,将数据序列化后再存储到缓存中去. 序列化这一操作,或许是显式的,或许是隐式的,这个取决于使用的package是否有帮我们做这样一件事. 本文会拿在.NET Core环境下使用Redis和Memcached来当例子说明,其中,Redis主要是用StackExchange.Redis,Memcached主要是用EnyimMemcachedCore. 先来看看一些我们常用的序列化方法. 常见的序列化方法 或许,比较常见的做法就是将一个对象序列

  • ASP.NET Core扩展库ServiceStack.Redis用法介绍

    给大家安利一款 ServiceStack.Redis 的 ASP.NET Core 扩展库,它是基于 ServiceStack.Redis.Core 开发的. 简单易用,开源免费,使用ASP.NET Core自身提供的DI容器来实现针对服务的注册和消费.直接在程序启动时注册到服务中即可完成全部配置,对于小白用户也可快速上手Redis缓存和Redis分布式缓存. Install Package https://www.nuget.org/packages/ServiceStack.Redis.Ex

  • .net core如何使用Redis发布订阅

    Redis是一个性能非常强劲的内存数据库,它一般是作为缓存来使用,但是他不仅仅可以用来作为缓存,比如著名的分布式框架dubbo就可以用Redis来做服务注册中心.接下来介绍一下.net core 使用Redis的发布/订阅功能. Redis 发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息. Redis 客户端可以订阅任意数量的通道. 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 -- client2 .

  • 详解如何在ASP.NET Core中使用Redis

    Redis 是一个开源的内存中的数据结构存储系统,可以用作数据库.缓存和消息中间件.它支持多种类型的数据结构:字符串,哈希表,列表,集合,有序集等等. Redis 官方没有推出Windows版本,倒是由Microsoft Open Tech提供了Windows 64bit 版本支持. 如何在Windows机器上安装Redis=>下载安装文件Redis-x64-3.2.100.msi,安装完毕之后,打开service管理器,找到Redis服务,并将其启动.  前期准备: 1.推荐使用Visual

  • .net core使用redis基于StackExchange.Redis

    .net core使用redis基于StackExchange.Redis教程,具体如下 一.添加引用包 StackExchange.Redis Microsoft.Extensions.Configuration 二.修改配置文件 appsettings.json { "RedisConfig": { "Redis_Default": { "Connection": "127.0.0.1: 6379", "Inst

  • Asp.net core中RedisMQ的简单应用实现

    最近一个外部的项目,使用到了消息队列,本来是用rabbitmq实现的,但是由于是部署到别人家的服务器上,想尽量简化一些,项目中本来也要接入了redis缓存,就尝试使用redis来实现简单的消息队列. 使用redis做消息队列有两种方法,一种是使用pub/sub,另一种是使用list结构,配合brpop来消费.这两种方式各有特点,这里简述一下: pub/sub模式,支持多客户端消费,但是不支持持久化,这就意味着客户端断开的时间内发布的消息将会全部舍弃掉. list配合brpop,默认不支持多客户端

  • Redis数据库基础与ASP.NET Core缓存实现

    目录 基础 Redis库 连接Redis 能用redis干啥 Redis数据库存储 字符串 订阅发布 RedisValue ASP.NETCore缓存与分布式缓存 内存中的缓存 ASP.NETCore的内存缓存 在内存中缓存.存储数据 IMemoryCache MemoryCache 分布式缓存 IDistributedCache Redis缓存 基础 Redis 库 C# 下 Redis-Client 开源的库很多,有 BeetleX.Redis.csredis.Nhiredis.redis-

  • 详解ASP.Net Core 中如何借助CSRedis实现一个安全高效的分布式锁

    引言:最近回头看了看开发的.Net Core 2.1项目的复盘总结,其中在多处用到Redis实现的分布式锁,虽然在OnResultExecuting方法中做了防止死锁的处理,但在某些场景下还是会发生死锁的问题,下面我只展示部分代码: 问题: (1)这里setnx设置的值"1",我想问,你最后del的这个值一定是你自己创建的吗? (2)图中标注的步骤1和步骤2不是原子操作,会有死锁的概率吗? 大家可以思考一下先,下面让我们带着这两个问题往下看,下面介绍一下使用Redis实现分布式锁常用的

  • .NET Core实现简单的Redis Client框架

    目录 0,关于RedisRESP 1,定义数据类型 2,定义异步消息状态机 3,定义命令发送模板 4,定义RedisClient 5,实现简单的RESP解析 6,实现命令发送客户端 7,如何使用 8,更多客户端 9,更多测试 10,性能测试 0,关于 Redis RESP RESP 全称 REdis Serialization Protocol ,即 Redis 序列化协议,用于协定客户端使用 socket 连接 Redis 时,数据的传输规则. 官方协议说明:https://redis.io/

  • 分享一个简单的java爬虫框架

    反复给网站编写不同的爬虫逻辑太麻烦了,自己实现了一个小框架 可以自定义的部分有: 请求方式(默认为Getuser-agent为谷歌浏览器的设置),可以通过实现RequestSet接口来自定义请求方式 储存方式(默认储存在f盘的html文件夹下),可以通过SaveUtil接口来自定义保存方式 需要保存的资源(默认为整个html页面) 筛选方式(默认所有url都符合要求),通过实现ResourseChooser接口来自定义需要保存的url和资源页面 实现的部分有: html页面的下载方式,通过Htt

  • Python Socket实现简单TCP Server/client功能示例

    本文实例讲述了Python Socket实现简单TCP Server/client功能.分享给大家供大家参考,具体如下: 网络上关于socket的介绍文章数不胜数.自己记录下学习的点点滴滴.以供将来复习学习使用. socket中文的翻译是套接字,总感觉词不达意.简单的理解就是ip+port形成的一个管理单元.也是程序中应用程序调用的接口. 在这里我们先介绍如何启动tcp 的server. tcp连接中server部分,启动一个ip和port口,在这个port口监听,当收到client发来的请求,

  • Android编程实现简单的UDP Client实例

    本文实例讲述了Android编程实现简单的UDP Client.分享给大家供大家参考,具体如下: 该代码在4.2.2内调试通过 1.记得加权限 <uses-permission android:name="android.permission.INTERNET"/> 注意:Android 4.0之后,就不能在主线程进行socket通信,否则会抛异常. 2.代码 MainActivity.java: package mao.example.quicksend; import

  • Core Java 简单谈谈HashSet(推荐)

    同学们在看这个问题的时候,我先提出者两个问题,然后大家带着问题看这个文章会理解的更好. 1.HashSet为什么添加元素时不能添加重复元素? 2.HashSet是否添加null元素? 打开源码, 我们看到如下代码,我们看到HashSet也有一个HashMap做为属性,HashSet()的构造方法就是将这个map实例化.如果大家对HashMap还不了解话,可以看我的这篇博文.还要注意有一个静态final的对象PRESENT,这个是干什么用的,咱们继续往下看. private transient H

  • 简单了解java ORM框架JOOQ

    这篇文章主要介绍了简单了解java ORM框架JOOQ,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 今天给大家介绍一个新的ORM框架->JOOQ,可能很多朋友还没有听说过这个框架,码农哥之前也是一直在使用Mybatis框架作为Java工程中的持久层访问框架,但是最近的一些项目采用JOOQ框架开发后,码农哥表示再也不想用Mybatis了! 为什么这么说呢?因为JOOQ在代码层面要比Mybatis简洁得多,而且性能也非常优异.相信大家都有过

  • PHP中迭代器的简单实现及Yii框架中的迭代器实现方法示例

    本文实例讲述了PHP中迭代器的简单实现及Yii框架中的迭代器实现方法.分享给大家供大家参考,具体如下: 在维基百科中我们可以看到其定义如下: 迭代器有时又称光标(cursor)是程式设计的软件设计模式,可在容器物件(container,例如list或vector)上遍访的接口,设计人员无需关心容器物件的内容. 各种语言实作Iterator的方式皆不尽同,有些面向对象语言像Java, C#, Python, Delphi都已将Iterator的特性内建语言当中,完美的跟语言整合,我们称之隐式迭代器

  • 简单了解前端渐进式框架VUE

    一.前端响应式框架VUE简介 Vue (读音 /vjuː/,类似于 view) Vue的官方网站是:https://cn.vuejs.org/ 是中国的大神尤雨溪开发的,为数不多的国人开发的世界顶级开源软件 是一套用于构建用户界面的渐进式框架.Vue 被设计为可以自底向上逐层应用.(下文会介绍什么是渐进式框架及自底向上逐层应用的概念) MVVM响应式编程模型,避免直接操作DOM , 降低DOM操作的复杂性. MVVM:页面输入改变数据,数据改变影响页面数据展示与渲染 M(model):普通的ja

  • 使用go net实现简单的redis通信协议

     图解redis通信协议 请求协议: 请求协议一般格式: *<参数数量> CR LF $<参数 1 的字节数量> CR LF <参数 1 的数据> CR LF ... $<参数 N 的字节数量> CR LF <参数 N 的数据> CR LF 例如,一个登录命令: *2 2-> 参数数量 $4 4-> 字节数量 AUTH $13 password@2018 返回结果: +OK 实际上,发送的命令为"*2\r\n$4\r\nAU

随机推荐