1. 简介
string 是 redis 最基本的类型,可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
2.使用场景
存储简单的键值对,比如我们需要统计某个网站的点击量,关注量、粉丝量等。
3.string数据结构示意图
string类型在存储数据时,是以key-value格式存储的,如下图所示:
基于本次Redis学习,首先创建一个项目,包含一个类库MyRedis.Redis和一个控制台应用程序MyRedis。项目结构如下:
需要在类库项目中安装这两个程序集:ServiceStack和ServiceStack.Redis
MyRedis.Redis类库项目说明:
InIt文件夹中的两个文件分别为RedisConfigInfo.cs和RedisManager.cs,这两个文件的作用分别是定义redis的配置信息和初始化redis链接池管理对象。代码如下:
RedisConfigInfo.cs
namespace MyRedis.Redis.Init { /// <summary> /// Redis配置信息 /// 也可以放到配置文件中 /// </summary> public class RedisConfigInfo { /// <summary> /// 默认端口6379 /// </summary> public string WriteServerList = "127.0.0.1:6379"; /// <summary> /// 可读的Redis链接地址 /// </summary> public string ReadServerList = "127.0.0.1:6379"; /// <summary> /// 最大写链接数 /// </summary> public int MaxWritePoolSize = 60; /// <summary> /// 最大读链接数 /// </summary> public int MaxReadPoolSize = 60; /// <summary> /// 本地缓存到期时间,单位:秒 /// </summary> public int LocalCacheTime = 180; /// <summary> /// 自动重启 /// </summary> public bool AutoStart = true; /// <summary> /// 是否记录日志,该设置仅用于排查Redis运行时出现的问题 /// 如果Redis工作正常,请关闭该项 /// </summary> public bool RecordeLog = false; } }
RedisManager.cs
using ServiceStack.Redis; namespace MyRedis.Redis.Init { /// <summary> /// Redis管理中心 /// </summary> public class RedisManager { /// <summary> /// redis配置文件信息 /// </summary> private static RedisConfigInfo RedisConfigInfo = new RedisConfigInfo(); /// <summary> /// redis客户端池化管理 /// </summary> private static PooledRedisClientManager prcManager; /// <summary> /// 静态构造方法,初始化链接池管理对象 /// </summary> static RedisManager() { CreateManager(); } /// <summary> ///创建链接池管理对象 /// </summary> private static void CreateManager() { string[] WriteServerConStr = RedisConfigInfo.WriteServerList.Split(','); string[] ReadServerConStr = RedisConfigInfo.ReadServerList.Split(','); prcManager = new PooledRedisClientManager(ReadServerConStr, WriteServerConStr, new RedisClientManagerConfig { MaxWritePoolSize = RedisConfigInfo.MaxWritePoolSize, MaxReadPoolSize = RedisConfigInfo.MaxReadPoolSize, AutoStart = RedisConfigInfo.AutoStart }); } /// <summary> /// 客户端缓存操作对象 /// </summary> /// <returns></returns> public static IRedisClient GetClient() { return prcManager.GetClient(); } } }
Interface文件夹中的RedisBase.cs,定义了是Redis操作的基类,继承自IDisposable接口,主要用于自动释放内存资源。代码如下:
using MyRedis.Redis.Init; using ServiceStack.Redis; using System; namespace MyRedis.Redis.Interface { /// <summary> /// RedisBase类,是Redis操作的基类,继承自IDisposable接口,主要用于释放内存 /// </summary> public abstract class RedisBase : IDisposable { public IRedisClient iClient { get; private set; } /// <summary> /// 构造时完成链接的打开 /// </summary> public RedisBase() { iClient = RedisManager.GetClient(); } private bool _disposed = false; protected virtual void Dispose(bool disposing) { if (!this._disposed) { if (disposing) { iClient.Dispose(); iClient = null; } } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// 自定义事物 /// </summary> public void Transcation() { using (IRedisTransaction irt = this.iClient.CreateTransaction()) { try { irt.QueueCommand(r => r.Set("key", 20)); irt.QueueCommand(r => r.Increment("key", 20)); irt.Commit();//提交事物 } catch (Exception ex) { irt.Rollback(); throw ex; } } } /// <summary> /// 清除全部数据 /// </summary> public virtual void FlushAll() { iClient.FlushAll(); } /// <summary> /// 保存数据DB文件到硬盘 /// </summary> public void Save() { iClient.Save();//阻塞式save } /// <summary> /// 异步保存数据DB文件到硬盘 /// </summary> public void SaveAsync() { iClient.SaveAsync();//异步save } } }
Service文件夹中的5个文件是对Redis中5中数据类型常用API方法的封装。
本次先看下RedisStringService.cs,此文件是对string数据类型中部分常用方法的封装。代码如下:
using MyRedis.Redis.Interface; using System; using System.Collections.Generic; namespace MyRedis.Redis.Service { public class RedisStringService : RedisBase { #region 赋值 /// <summary> /// 设置key的value /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public bool Set<T>(string key, T value) { return base.iClient.Set<T>(key, value); } /// <summary> /// 设置key的value并设置过期时间 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <param name="dt"></param> /// <returns></returns> public bool Set<T>(string key, T value, DateTime dt) { return base.iClient.Set<T>(key, value, dt); } /// <summary> /// 设置key的value并设置过期时间 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <param name="dt"></param> /// <returns></returns> public bool Set<T>(string key, T value, TimeSpan sp) { return base.iClient.Set<T>(key, value, sp); } /// <summary> /// 设置多个key/value /// </summary> /// <param name="dic"></param> public void Set(Dictionary<string, string> dic) { base.iClient.SetAll(dic); } #endregion #region 追加 /// <summary> /// 在原有的key的value值之后追加value,没有就新增一项 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public long Append(string key, string value) { return base.iClient.AppendToValue(key, value); } #endregion #region 获取值 /// <summary> /// 获取key的value /// </summary> /// <param name="key"></param> /// <returns></returns> public string Get(string key) { return base.iClient.GetValue(key); } /// <summary> /// 获取多个key的values值 /// </summary> /// <param name="keys"></param> /// <returns></returns> public List<string> Get(List<string> keys) { return base.iClient.GetValues(keys); } /// <summary> /// 获取多个key的values值(泛型) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="keys"></param> /// <returns></returns> public List<T> Get<T>(List<string> keys) { return base.iClient.GetValues<T>(keys); } #endregion #region 获取旧值赋上新值 /// <summary> /// 获取旧值赋上新值 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public string GetAndSetValue(string key, string value) { return base.iClient.GetAndSetValue(key, value); } #endregion #region 辅助方法 /// <summary> /// 获取值的长度 /// </summary> /// <param name="key"></param> /// <returns></returns> public long GetLength(string key) { return base.iClient.GetStringCount(key); } /// <summary> /// 自增1,返回自增后的值 /// </summary> /// <param name="key"></param> /// <returns></returns> public long Incr(string key) { return base.iClient.IncrementValue(key); } /// <summary> /// 自增count,返回自增后的值 /// </summary> /// <param name="key"></param> /// <param name="count"></param> /// <returns></returns> public long IncrBy(string key, int count) { return base.iClient.IncrementValueBy(key, count); } /// <summary> /// 自减1,返回自减后的值 /// </summary> /// <param name="key"></param> /// <returns></returns> public long Decr(string key) { return base.iClient.DecrementValue(key); } /// <summary> /// 自减count,返回自减后的值 /// </summary> /// <param name="key"></param> /// <param name="count"></param> /// <returns></returns> public long DecrBy(string key, int count) { return base.iClient.DecrementValueBy(key, count); } #endregion } }
上面的代码创建完成后,项目的基础就构建完成了,下面开始学习string数据类型常用api的使用。
在控制台应用程序中创建ServiceStackTest.cs类,用于调用Redis的api方法。代码如下:
using MyRedis.Redis.Service; using System; using System.Threading; namespace MyRedis { /// <summary> /// Redis常用api调用 /// </summary> public class ServiceStackTest { public class Student { public int Id { get; set; } public string Name { get; set; } public string Remark { get; set; } public int Age { get; set; } } public static void Show() { Student student1 = new Student() { Id = 11, Name = "张三", Remark = "测试备注张三信息", Age = 20 }; Student student2 = new Student() { Id = 22, Name = "李四", Remark = "测试备注李四信息", Age = 30 }; #region RedisStringService using (RedisStringService service = new RedisStringService()) { service.Set<string>("s", "测试"); service.Set<Student>("student", student1); Console.WriteLine(service.Get("s")); service.Append("s", "123456"); Console.WriteLine(service.Get("s")); Console.WriteLine(service.GetAndSetValue("s", "嘿嘿")); Console.WriteLine(service.Get("s")); service.Set<string>("s1", "张", DateTime.Now.AddSeconds(5));//设置过期时间为5秒钟 Thread.Sleep(5100); Console.WriteLine(service.Get("s1"));//数据已过期,在redis中查询不到key为s1的数据 service.Set<int>("Age", 22); Console.WriteLine(service.Incr("Age")); Console.WriteLine(service.IncrBy("Age", 3)); Console.WriteLine(service.Decr("Age")); Console.WriteLine(service.DecrBy("Age", 3)); } #endregion } } }
main方法调用代码:
using System; using MyRedis.Demo; namespace MyRedis { class Program { static void Main(string[] args) { try { ServiceStackTest.Show(); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } } }
上面代码执行过程中可以看具体方法的执行情况,这里就不一一调试了,执行后可以从可视化工具看到Redis中存储的数据信息:
在实际应用场景中,可能会用Redis解决商品超卖的问题,在秒杀商品时,同一时刻有多个用户购买同一件商品。为了防止超卖情况方式,可以使用Redis防止超卖。可以看下面一个实例,模拟5000个用户同时抢购10件商品:
OverSellDemo.cs
using MyRedis.Redis.Service; using System; using System.Threading.Tasks; namespace MyRedis.Demo { /// <summary> /// 使用Redis防超卖实现 /// </summary> public class OverSellDemo { private static bool IsGoOn = true;//秒杀活动是否结束 public static void Show() { try { using (RedisStringService service = new RedisStringService()) { service.Set<int>("Stock", 10);//代表库存 } for (int i = 0; i < 5000; i++)//模拟5000个用户同时访问 { int k = i; //一个线程就是一个用户请求 Task.Run(() => { using (RedisStringService service = new RedisStringService()) { if (IsGoOn) { //Redis是单线程处理 所有不会有同时-1 long index = service.Decr("Stock");//-1并返回 if (index >= 0) { Console.WriteLine($"{k.ToString("000")} 秒杀成功,商品为{index}"); //去数据库增加订单等等操作 } else { if (IsGoOn) { IsGoOn = false; } Console.WriteLine($"{k.ToString("000")} 秒杀失败,商品为{index}"); } } else { Console.WriteLine($"{k.ToString("000")} 库存不足 秒杀停止"); } } }); } } catch (Exception ex) { Console.WriteLine(ex.Message); throw ex; } } } }
执行结果:
从结果可以看到商品只有10件被卖出去了,后面的请求中有两个请求秒杀失败,其他的都是返回库存不足。这不仅达到了防止超卖的目的,同时也阻挡了大量无效请求,大大降低了数据库的压力。在Redis缓存中没有库存的情况下,程序不会去调用数据库。
上面的实例中,Redis可以做到防超卖的原因是Redis是原子性操作,保证一个数值只会出现一次,防止一个商品卖给多个人。
下面比较下不使用Redis时高并发和使用Redis高并发的秒杀商品的两种情况:
假如秒杀分为3个步骤:1.获取库存 2.程序中对库存减一 3.将减一后的结果保存
不使用Redis:(以3个线程并发为例)
A线程 步骤1→ 步骤2→ 步骤3 减一后结果为9
B线程 步骤1→ 步骤2→ 步骤3 减一后结果可能为9,可能为8
C线程 步骤1→ 步骤2→ 步骤3 减一后结果可能为9,可能为8,可能为7
使用Redis:(以3个线程并发为例)
A线程 步骤1→ 步骤2→ 步骤3 减一后结果为9
B线程 步骤1→ 步骤2→ 步骤3 减一后结果为8
C线程 步骤1→ 步骤2→ 步骤3 减一后结果为7
造成这样结果的原因就是步骤1,2,3在不使用Redis情况下,三个操作是不连续的,三个线程可能同时做步骤一,也可能同时做步骤二、步骤三。使用Redis后就不一样了,Redis是单线程,一次只可能做一个步骤,只有一个线程的三个步骤执行完毕,再去执行下一个线程的三个步骤。