Redis中的String类型及使用Redis解决订单秒杀超卖问题

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

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

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

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

此处推荐使用的是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中的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解决订单秒杀超卖问题:

/// <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能够很好的解决订单秒杀超卖问题。

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

demo源码:

复制代码 代码如下: 链接:https://pan.baidu.com/s/1qbhqywqfhqsasy-nwsfrra 提取码:78so

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

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

(0)
上一篇 2022年3月21日
下一篇 2022年3月21日

相关推荐