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个线程并发,初始值为10)
A线程 步骤1→ 步骤2→ 步骤3 减一后结果为9
B线程 步骤1→ 步骤2→ 步骤3 减一后结果可能为9,可能为8
C线程 步骤1→ 步骤2→ 步骤3 减一后结果可能为9,可能为8,可能为7
使用Redis:(3个线程并发,初始值为10)
A线程 步骤1→ 步骤2→ 步骤3 减一后结果为9
B线程 步骤1→ 步骤2→ 步骤3 减一后结果为8
C线程 步骤1→ 步骤2→ 步骤3 减一后结果为7
造成这样结果的原因就是步骤1、2、3在不使用Redis情况下,三个操作是不连续的,三个线程可能同时做步骤一,也可能同时做步骤二、步骤三。
使用Redis后就不一样了,Redis是单线程,一次只可能做一个步骤,只有一个线程的三个步骤执行完毕,再去执行下一个线程的三个步骤。这里不用考虑Redis单线程执行效率问题,虽然Redis是单线程执行,但是效率非常快。