Redis学习系列二之.Net开发环境搭建及基础数据结构String字符串

一、简介

Redis有5种基本数据结构,分别是string、list(列表)、hash(字典)、set(集合)、zset(有序集合),这是必须掌握的5种基本数据结构.注意Redis作为一个键值对缓存系统,其所有的数据结构,都以唯一的key(字符串)作为名称,然后通过key来获取对应的数据.

二、.Net开发环境搭建

这个版本,暂时不考虑并发问题,后续的文章会说! 第一步:安装StackExchange.Redis包,我用的是2.0.519版本的.

第二步:编写代码,采用扩展方法的链式编程模式+async/await的编程模型

AppConfiguration.cs  全局配置类

    /// <summary>
    /// 全局配置类
    /// </summary>
    public class AppConfiguration
    {
        /// <summary>
        /// 单例实现,static关键字默认加锁
        /// </summary>
        static AppConfiguration()
        {
            Current = new AppConfiguration();
        }

        public static readonly AppConfiguration Current;

        /// <summary>
        /// 配置完Redis之后,所有需要的Redis基础服务对象,都在这里面
        /// </summary>
        public RedisConfigurations RedisConfigurations { get; set; }
    }

FluentConfiguration.cs 链式配置核心类

    /// <summary>
    /// 链式编程模式,扩展方法实现
    /// </summary>
    public static class FluentConfiguration
    {
        /// <summary>
        /// 配置Redis
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static AppConfiguration ConfigureRedis<T>(this AppConfiguration configuration) where T: IRedisConfig, new()
        {
            if (configuration == null)
                throw new ArgumentNullException("configuration");
            var config = new T();
            var redisConfigurations=config.ConfigRedis();
            configuration.RedisConfigurations = redisConfigurations;
            return configuration;
        }
    }

RedisConfigurations.cs Redis全局配置共享类

    /// <summary>
    /// Redis配置完毕后,返回需要使用的相关对象
    /// </summary>
    public class RedisConfigurations
    {
        public IConnectionMultiplexer ConnectionMultiplexer { get; set; }
    }

RedisConfig.cs Redis配置类

    /// <summary>
    /// Redis配置类
    /// </summary>
    public class RedisConfig : IRedisConfig
    {
        /// <summary>
        /// 比较耗费资源,所以写入缓存,全局共享
        /// 封装了Redis基础服务对象的详细信息
        /// </summary>
        public static ConnectionMultiplexer ConnectionMultiplexer { get; }

        /// <summary>
        /// 可能存在线程安全的配置,或者只需要初始化一次的配置,放这里
        /// </summary>
        static RedisConfig()
        {
            //暂时读配置文件,后期可以用Core的配置文件系统读json文件
            var redisServerAdress = ConfigurationManager.AppSettings["RedisServerAdress"];
            if (string.IsNullOrEmpty(redisServerAdress))
                throw new ApplicationException("配置文件中未找到RedisServer的有效配置");
            ConnectionMultiplexer = ConnectionMultiplexer.Connect(redisServerAdress);
        }

        /// <summary>
        /// 配置Redis
        /// </summary
        public RedisConfigurations ConfigRedis()
        {
            var config = new RedisConfigurations();
            config.ConnectionMultiplexer = ConnectionMultiplexer;
            return config;
        }
    }

相关约束接口如下:

    /// <summary>
    /// Redis配置约束
    /// </summary>
    public interface IRedisConfig
    {
        /// <summary>
        /// 配置Redis
        /// </summary>
        RedisConfigurations ConfigRedis();
    }

    /// <summary>
    /// Redis客户端实例约束接口
    /// </summary>
    public interface IRedisInstance
    {

    }

RedisClient.cs Redis客户端调用类

    /// <summary>
    /// 基于async和await的异步操作的Redis客户端,有效利用CPU资源
    /// </summary>
    public class RedisClient: IRedisInstance
    {
        private static RedisConfigurations RedisConfigurations { get; }

        static RedisClient()
        {
            RedisConfigurations=AppConfiguration.Current.RedisConfigurations;
        }

        /// <summary>
        /// 异步,写入键值对
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static async Task<bool> StringSetAsync(string key,string value)
        {
            var db=GetDatabase();
            return await db.StringSetAsync(key,value);
        }

        /// <summary>
        /// 根据传入键,异步获取对应的值
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static async Task<string> StringGetAsync(string key)
        {
            var db = GetDatabase();
            return await db.StringGetAsync(key);
        }

        /// <summary>
        /// 异步判断是否存在某个键
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static async Task<bool> KeyExistsAsync(string key)
        {
            var db = GetDatabase();
            return await db.KeyExistsAsync(key);
        }

        /// <summary>
        /// 异步删除某个键
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static async Task<bool> KeyDeleteAsync(string key)
        {
            var db = GetDatabase();
            return await db.KeyDeleteAsync(key);
        }

        /// <summary>
        /// Redis DataBase工厂方法
        /// </summary>
        /// <returns></returns>
        private static IDatabase GetDatabase()
        {
            return RedisConfigurations.ConnectionMultiplexer.GetDatabase();
        }
    }

暂时只扩展了一些方法,或许会持续扩展.

Program.cs 控制台入口类

    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
            if (await RedisClient.StringSetAsync("name", "xiaochao"))
            {
                Console.WriteLine("Redis中键为name的值为:{0}", await RedisClient.StringGetAsync("name"));
            }
            else {
                Console.WriteLine("写入异常");
            }
        }
    }

ok,到这里.Net下使用StackExchange.Redis包操作Redis的环境构建完毕.

运行代码:

控制台环境:

Redis桌面管理工具

Linux下Redis-cli

后续的文章都会围绕上面三个操作方式展开.

三、string(字符串)

1、简单键值对操作

字符串string是Redis中最简单的数据类型,内部原理和C#的string类型一样,是一个字符数组.常见的用法是缓存一些用户数据,将用户数据序列化程Json,然后以用户Id作为键值,然后将用户数据存入Redis中.获取的时候,只需要通过用户Id去获取,然后将Json反序列化成对应的实体.

注:Redis的string类型是动态字符串,而且支持修改,这和C#中的string不一样,内部结构类似于C#中的List,有一个初始大小,如果存入string的长度大小大于string的初始大小,那么每次都会扩展1倍的大小.但是字符串最大长度只能为512MB.

代码实战:

(1)、Linux终端

(2)、C#控制台

修改控制台方法如下:

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
            var key = "name";
            if (await RedisClient.StringSetAsync(key, "xiaochao"))
            {
                Console.WriteLine("Redis中键为name的值为:{0}", await RedisClient.StringGetAsync(key));
                if (await RedisClient.KeyExistsAsync(key))
                {
                    Console.WriteLine("Redis中,存在key为name的键值对");
                }
                if (await RedisClient.KeyDeleteAsync(key))
                {
                    Console.WriteLine($"删除键:{key}成功");
                    if (await RedisClient.KeyExistsAsync(key))
                        Console.WriteLine($"{key}存在,删除失败");
                    else
                        Console.WriteLine($"{key}不存在了,被删除了");
                }
            }
            else {
                Console.WriteLine("写入异常");
            }
        }

桌面管理工具:

2、批量键值对操作

C#控制台:首先引入Newtonsoft.Json包

修改RedisClient.cs如下,给它扩展两个方法

        /// <summary>
        /// 异步批量插入键值对
        /// </summary>
        /// <param name="keyValuePair"></param>
        /// <returns></returns>
        public static async Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] keyValuePair)
        {
            var db = GetDatabase();
            return await db.StringSetAsync(keyValuePair);
        }

        /// <summary>
        /// 异步批量获取值
        /// </summary>
        /// <param name="keyValuePair"></param>
        /// <returns></returns>
        public static async Task<RedisValue[]> StringGetAsync(RedisKey[] keys)
        {
            var db = GetDatabase();
            return await db.StringGetAsync(keys);
        }

Program.cs如下:

    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
            var userInfos = UserInfo.UserInfos;
            var keyValues = new KeyValuePair<RedisKey, RedisValue>[userInfos.Count];
            var keys =new RedisKey[userInfos.Count];
            for (var i = 0; i < userInfos.Count; i++)
            {
                var currUserInfo = userInfos[i];
                var key = currUserInfo.Id.ToString();
                var value = JsonConvert.SerializeObject(currUserInfo);
                keyValues[i] = new KeyValuePair<RedisKey, RedisValue>(key, value);
                keys[i] = key;
            }
            if (await RedisClient.StringSetAsync(keyValues))
            {
                try
                {
                    var values = await RedisClient.StringGetAsync(keys);
                    for (var i = 0; i < values.Length; i++)
                    {
                        Console.WriteLine(values[i]);
                    }
                }
                //捕获辅助线程产生的异常
                catch (AggregateException ex)
                {
                    ex.Handle(x =>
                    {
                        //记录日志
                        Console.WriteLine("异常处理完毕,批量获取值失败!");
                        return true;
                    });
                }
            }
            else
            {
                //记录日志
                Console.WriteLine("写入异常");
            }
        }

        class UserInfo
        {

            public Guid Id { get; set; }

            public string Name { get; set; }

            public int Age { get; set; }

            internal static List<UserInfo> UserInfos = new List<UserInfo>()
            {
                new UserInfo()
                {
                    Id=Guid.NewGuid(),
                    Name="小超",
                    Age=23
                },
                 new UserInfo()
                {
                    Id=Guid.NewGuid(),
                    Name="大超",
                    Age=23
                },
            };

        }
    }

(2)、管理工具

(3)、Linux终端

3、过期时间

Redis可以给Key设置过期时间,到达设置的时间,对应的键值对会被删除,内存会被回收,这个功能常用来控制缓存的失效时间.这里这个自动删除的机制很复杂,这里不想说太多,只介绍基本用法,后续的文章会介绍.

C#控制台,修改RedisClient.cs中的StringSetAsync方法如下:

        /// <summary>
        /// 异步,写入键值对,可指定过期时间
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="expireTime">过期时间</param>
        /// <returns></returns>
        public static async Task<bool> StringSetAsync(string key,string value, TimeSpan? expireTime=null)
        {
            var db=GetDatabase();
            return await db.StringSetAsync(key,value, expireTime);
        }

Program.cs代码如下:

    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
         
            if (await RedisClient.StringSetAsync("name","xiaochao",TimeSpan.FromSeconds(2)))
            {
                Console.WriteLine("Redis中存在键为name的键值对,值为:{0}",await RedisClient.StringGetAsync("name"));
                await Task.Delay(2000);//模拟休息两秒
                Console.WriteLine("休息两秒后,Redis的键为name的键值对:{0}", string.IsNullOrEmpty(await RedisClient.StringGetAsync("name")) ? "不存在" : "存在");
            }
            else
            {
                //记录日志
                Console.WriteLine("写入异常");
            }
        }
    }

这边其它两个终端就不演示了,自行观察.

4、计数器

Redis提供了自增命令,前提操作的数据必须是整数,而且自增是有范围的.默认对应long的最大值,一般达不到这个值.

C#控制台:

修改RedisClient.cs下的StringSetAsync方法如下:

        /// <summary>
        /// 异步,写入键值对,可指定过期时间
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="expireTime">过期时间</param>
        /// <returns></returns>
        public static async Task<bool> StringSetAsync(string key,RedisValue value, TimeSpan? expireTime=null)
        {
            var db=GetDatabase();
            return await db.StringSetAsync(key, value, expireTime);
        }

Program.cs代码如下:

    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
         
            if (await RedisClient.StringSetAsync("站点首页",0))
            {
               
                    //模拟用户访问
                    Parallel.For(0, 250000, async (i, ParallelLoopState) =>
                     {
                         try
                         {
                             await RedisClient.StringIncrementAsync("站点首页");
                         }
                         catch (RedisServerException ex)
                         {
                             //记录日志
                             Console.WriteLine(ex.Message);
                             ParallelLoopState.Stop();
                             return;
                         }
                     });
                    //输出站点的UV
                    Console.WriteLine(await RedisClient.StringGetAsync("站点首页"));
            }
            else
            {
                //记录日志
                Console.WriteLine("写入异常");
            }
        }
    }

注:这里存在两个问题,如果你把Parallel的上限值设置的过大,那么短时间内,可能Redis无法处理这么多的并发量,而报超时错误,这个时候,解决方案是使用集群的方式,解决这个问题,但是这里就不演示了.第二个问题是,如果把Set的初始值设为Long.MaxValue,那么Redis会报溢出错误,上面的代码已经处理.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券