Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

  • A+
所属分类:.NET技术
摘要

本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的String类型,以及如何使用Redis解决订单秒杀超卖问题。

本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的String类型,以及如何使用Redis解决订单秒杀超卖问题。

Redis中5种数据结构之String类型:key-value的缓存,支持过期,value不超过512M。

Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

Redis是单线程的,比如SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy等等,这些看上去像是组合命令,但实际上是一个具体的命令,是一个原子性的命令,不可能出现中间状态,可以应对一些并发情况。下面我们直接通过代码来看下具体使用。

首先来看下Demo的项目结构:

Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

此处推荐使用的是ServiceStack包,虽然它是收费的,有1小时3600次请求限制,但是它是开源的,可以将它的源码下载下来破解后使用,网上应该有挺多相关资料,有兴趣的可以去了解一波。

一、Redis中与String类型相关的API

首先先来看下Redis客户端的初始化工作:

using System;  namespace TianYa.Redis.Init {     /// <summary>     /// redis配置文件信息     /// 也可以放到配置文件去     /// </summary>     public sealed class RedisConfigInfo     {         /// <summary>         /// 可写的Redis链接地址         /// format:ip1,ip2         ///          /// 默认6379端口         /// </summary>         public string WriteServerList = "127.0.0.1:6379";          /// <summary>         /// 可读的Redis链接地址         /// format:ip1,ip2         ///          /// 默认6379端口         /// </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;     } }

using ServiceStack.Redis;  namespace TianYa.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>         public static IRedisClient GetClient()         {             return _prcManager.GetClient();         }     } }

using System; using TianYa.Redis.Init; using ServiceStack.Redis;  namespace TianYa.Redis.Service {     /// <summary>     /// redis操作的基类     /// </summary>     public abstract class RedisBase : IDisposable     {         /// <summary>         /// Redis客户端         /// </summary>         protected IRedisClient _redisClient { get; private set; }          /// <summary>         /// 构造函数         /// </summary>         public RedisBase()         {             this._redisClient = RedisManager.GetClient();         }          private bool _disposed = false;         protected virtual void Dispose(bool disposing)         {             if (!this._disposed)             {                 if (disposing)                 {                     _redisClient.Dispose();                     _redisClient = null;                 }             }              this._disposed = true;         }          public void Dispose()         {             Dispose(true);             GC.SuppressFinalize(this);         }          /// <summary>         /// Redis事务处理示例         /// </summary>         public void Transcation()         {             using (IRedisTransaction irt = this._redisClient.CreateTransaction())             {                 try                 {                     irt.QueueCommand(r => r.Set("key", 20));                     irt.QueueCommand(r => r.Increment("key", 1));                     irt.Commit(); //事务提交                 }                 catch (Exception ex)                 {                     irt.Rollback(); //事务回滚                     throw ex;                 }             }         }          /// <summary>         /// 清除全部数据 请小心         /// </summary>         public virtual void FlushAll()         {             _redisClient.FlushAll();         }          /// <summary>         /// 保存数据DB文件到硬盘         /// </summary>         public void Save()         {             _redisClient.Save(); //阻塞式Save         }          /// <summary>         /// 异步保存数据DB文件到硬盘         /// </summary>         public void SaveAsync()         {             _redisClient.SaveAsync(); //异步Save         }     } }

下面直接给大家Show一波Redis中与String类型相关的API:

using System; using System.Collections.Generic;  namespace TianYa.Redis.Service {     /// <summary>     /// key-value 键值对 value可以是序列化的数据 (字符串)     /// </summary>     public class RedisStringService : RedisBase     {         #region 赋值          /// <summary>         /// 设置永久缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="value">存储的值</param>         /// <returns></returns>         public bool Set(string key, string value)         {             return base._redisClient.Set(key, value);         }          /// <summary>         /// 设置永久缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="value">存储的值</param>         /// <returns></returns>         public bool Set<T>(string key, T value)         {             return base._redisClient.Set<T>(key, value);         }          /// <summary>         /// 带有过期时间的缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="value">存储的值</param>         /// <param name="expireTime">过期时间</param>         /// <returns></returns>         public bool Set(string key, string value, DateTime expireTime)         {             return base._redisClient.Set(key, value, expireTime);         }          /// <summary>         /// 带有过期时间的缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="value">存储的值</param>         /// <param name="expireTime">过期时间</param>         /// <returns></returns>         public bool Set<T>(string key, T value, DateTime expireTime)         {             return base._redisClient.Set<T>(key, value, expireTime);         }          /// <summary>         /// 带有过期时间的缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="value">存储的值</param>         /// <param name="expireTime">过期时间</param>         /// <returns></returns>         public bool Set<T>(string key, T value, TimeSpan expireTime)         {             return base._redisClient.Set<T>(key, value, expireTime);         }          /// <summary>         /// 设置多个key/value         /// </summary>         public void SetAll(Dictionary<string, string> dic)         {             base._redisClient.SetAll(dic);         }          #endregion 赋值          #region 追加          /// <summary>         /// 在原有key的value值之后追加value,没有就新增一项         /// </summary>         public long AppendToValue(string key, string value)         {             return base._redisClient.AppendToValue(key, value);         }          #endregion 追加          #region 获取值          /// <summary>         /// 读取缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public string Get(string key)         {             return base._redisClient.GetValue(key);         }          /// <summary>         /// 读取缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public T Get<T>(string key)         {             return                 _redisClient.ContainsKey(key)                 ? _redisClient.Get<T>(key)                 : default;         }          /// <summary>         /// 获取多个key的value值         /// </summary>         /// <param name="keys">存储的键集合</param>         /// <returns></returns>         public List<string> Get(List<string> keys)         {             return base._redisClient.GetValues(keys);         }          /// <summary>         /// 获取多个key的value值         /// </summary>         /// <param name="keys">存储的键集合</param>         /// <returns></returns>         public List<T> Get<T>(List<string> keys)         {             return base._redisClient.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._redisClient.GetAndSetValue(key, value);         }          #endregion 获取旧值赋上新值          #region 移除缓存          /// <summary>         /// 移除缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public bool Remove(string key)         {             return _redisClient.Remove(key);         }          /// <summary>         /// 移除多个缓存         /// </summary>         /// <param name="keys">存储的键集合</param>         public void RemoveAll(List<string> keys)         {             _redisClient.RemoveAll(keys);         }          #endregion 移除缓存          #region 辅助方法          /// <summary>         /// 是否存在缓存         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public bool ContainsKey(string key)         {             return _redisClient.ContainsKey(key);         }          /// <summary>         /// 获取值的长度         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public long GetStringCount(string key)         {             return base._redisClient.GetStringCount(key);         }          /// <summary>         /// 自增1,返回自增后的值         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public long IncrementValue(string key)         {             return base._redisClient.IncrementValue(key);         }          /// <summary>         /// 自增count,返回自增后的值         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="count">自增量</param>         /// <returns></returns>         public long IncrementValueBy(string key, int count)         {             return base._redisClient.IncrementValueBy(key, count);         }          /// <summary>         /// 自减1,返回自减后的值         /// </summary>         /// <param name="key">存储的键</param>         /// <returns></returns>         public long DecrementValue(string key)         {             return base._redisClient.DecrementValue(key);         }          /// <summary>         /// 自减count,返回自减后的值         /// </summary>         /// <param name="key">存储的键</param>         /// <param name="count">自减量</param>         /// <returns></returns>         public long DecrementValueBy(string key, int count)         {             return base._redisClient.DecrementValueBy(key, count);         }          #endregion 辅助方法     } }

测试如下:

using System;  namespace MyRedis {     /// <summary>     /// 学生类     /// </summary>     public class Student     {         public int Id { get; set; }         public string Name { get; set; }         public string Remark { get; set; }         public string Description { get; set; }     } }

using System; using System.Collections.Generic; using TianYa.Redis.Service; using Newtonsoft.Json;  namespace MyRedis {     /// <summary>     /// ServiceStack API封装测试  五大结构理解 (1小时3600次请求限制--可破解)     /// </summary>     public class ServiceStackTest     {         /// <summary>         /// String         /// key-value的缓存,支持过期,value不超过512M         /// Redis是单线程的,比如SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy,         /// 这些看上去是组合命令,但实际上是一个具体的命令,是一个原子性的命令,不可能出现中间状态,可以应对一些并发情况         /// </summary>         public static void ShowString()         {             var student1 = new Student()             {                 Id = 10000,                 Name = "TianYa"             };              using (RedisStringService service = new RedisStringService())             {                 service.Set("student1", student1);                 var stu = service.Get<Student>("student1");                 Console.WriteLine(JsonConvert.SerializeObject(stu));                  service.Set<int>("Age", 28);                 Console.WriteLine(service.IncrementValue("Age"));                 Console.WriteLine(service.IncrementValueBy("Age", 3));                 Console.WriteLine(service.DecrementValue("Age"));                 Console.WriteLine(service.DecrementValueBy("Age", 3));             }         }     } }

using System;  namespace MyRedis {     /// <summary>     /// Redis:Remote Dictionary Server 远程字典服务器     /// 基于内存管理(数据存在内存),实现了5种数据结构(分别应对各种具体需求),单线程模型的应用程序(单进程单线程),对外提供插入--查询--固化--集群功能。     /// 正是因为基于内存管理所以速度快,可以用来提升性能。但是不能当数据库,不能作为数据的最终依据。     /// 单线程多进程的模式来提供集群服务。     /// 单线程最大的好处就是原子性操作,就是要么都成功,要么都失败,不会出现中间状态。Redis每个命令都是原子性(因为单线程),不用考虑并发,不会出现中间状态。(线程安全)     /// Redis就是为开发而生,会为各种开发需求提供对应的解决方案。     /// Redis只是为了提升性能,不做数据标准。任何的数据固化都是由数据库完成的,Redis不能代替数据库。     /// Redis实现的5种数据结构:String、Hashtable、Set、ZSet和List。     /// </summary>     class Program     {         static void Main(string[] args)         {             ServiceStackTest.ShowString();             Console.ReadKey();         }     } }

运行结果如下:

Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

Redis中的String类型在项目中使用是最多的,想必大家都有所了解,此处就不再做过多的描述了。

二、使用Redis解决订单秒杀超卖问题

首先先来看下什么是订单秒杀超卖问题:

/// <summary> /// 模拟订单秒杀超卖问题 ///     超卖:订单数超过商品 ///     如果使用传统的锁来解决超卖问题合适吗?  ///         不合适,因为这个等于是单线程了,其他都要阻塞,会出现各种超时。 ///         -1的时候除了操作库存,还得增加订单,等支付等等。 ///         10个商品秒杀,一次只能进一个? 违背了业务。 /// </summary> public class OverSellFailedTest {     private static bool _isGoOn = true; //秒杀活动是否结束     private static int _stock = 0; //商品库存     public static void Show()     {         _stock = 10;         for (int i = 0; i < 5000; i++)         {             int k = i;             Task.Run(() => //每个线程就是一个用户请求             {                 if (_isGoOn)                 {                     long index = _stock;                     Thread.Sleep(100); //模拟去数据库查询库存                     if (index >= 1)                     {                         _stock = _stock - 1; //更新库存                         Console.WriteLine($"{k.ToString("0000")}秒杀成功,秒杀商品索引为{index}");                         //可以分队列,去操作数据库                     }                     else                     {                         if (_isGoOn)                         {                             _isGoOn = false;                         }                          Console.WriteLine($"{k.ToString("0000")}秒杀失败,秒杀商品索引为{index}");                     }                 }                 else                 {                     Console.WriteLine($"{k.ToString("0000")}秒杀停止......");                 }             });         }     } }

运行OverSellFailedTest.Show(),结果如下所示:

Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

从运行结果可以看出不仅一个商品卖给了多个人,而且还出现了订单数超过商品数,这就是典型的秒杀超卖问题。

下面我们来看下如何使用Redis解决订单秒杀超卖问题:

/// <summary> /// 使用Redis解决订单秒杀超卖问题 ///     超卖:订单数超过商品 ///     1、Redis原子性操作--保证一个数值只出现一次--防止一个商品卖给多个人 ///     2、用上了Redis,一方面保证绝对不会超卖,另一方面没有效率影响,还有撤单的时候增加库存,可以继续秒杀, ///        限制秒杀的库存是放在redis,不是数据库,不会造成数据的不一致性 ///     3、Redis能够拦截无效的请求,如果没有这一层,所有的请求压力都到数据库 ///     4、缓存击穿/穿透---缓存down掉,请求全部到数据库 ///     5、缓存预热功能---缓存重启,数据丢失,多了一个初始化缓存数据动作(写代码去把数据读出来放入缓存) /// </summary> public class OverSellTest {     private static bool _isGoOn = true; //秒杀活动是否结束     public static void Show()     {         using (RedisStringService service = new RedisStringService())         {             service.Set<int>("Stock", 10); //库存         }          for (int i = 0; i < 5000; i++)         {             int k = i;             Task.Run(() => //每个线程就是一个用户请求             {                 using (RedisStringService service = new RedisStringService())                 {                     if (_isGoOn)                     {                         long index = service.DecrementValue("Stock"); //减1并且返回                           if (index >= 0)                         {                             Console.WriteLine($"{k.ToString("0000")}秒杀成功,秒杀商品索引为{index}");                             //service.IncrementValue("Stock"); //加1,如果取消了订单则添加库存继续秒杀                             //可以分队列,去操作数据库                         }                         else                         {                             if (_isGoOn)                             {                                 _isGoOn = false;                             }                              Console.WriteLine($"{k.ToString("0000")}秒杀失败,秒杀商品索引为{index}");                         }                     }                     else                     {                         Console.WriteLine($"{k.ToString("0000")}秒杀停止......");                     }                 }             });         }     } }

运行OverSellTest.Show(),结果如下所示:

Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

从运行结果可以看出使用Redis能够很好的解决订单秒杀超卖问题。

至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!!

 

Demo源码:

链接:https://pan.baidu.com/s/1qbHQywQfhQSaSY-nwsFRrA  提取码:78so

此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/13979522.html

版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!