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)

相关推荐

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

    本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的String类型,以及如何使用Redis解决订单秒杀超卖问题. Redis中5种数据结构之String类型:key-value的缓存,支持过期,value不超过512M. Redis是单线程的,比如SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy等等,这些看上去像是组合命

  • Redis中一个String类型引发的惨案

    ​ 曾经看到这么一个案例,有一个团队需要开发一个图片存储系统,要求这个系统能快速记录图片ID和图片存储对象ID,同时还需要能够根据图片的ID快速找到图片存储对象ID.我们假设用10位数来表示图片ID和图片存储对象ID,例如图片的ID为1101021043,它所对应的图片存储对象的ID为2301010051,可以看到图片ID和图片存储ID正好是一一对应的,是典型的key-value形式,所以首先会想到直接使用String类型来保存数据.把图片ID和图片存储ID分别作为键值对的key和value来保

  • 详解Redis中的List类型

    本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的List类型,以及如何使用Redis解决博客数据分页.生产者消费者模型和发布订阅等问题. Redis List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用这个数据结构. List类型主要用于队列和栈,先进先出,后进先出等. 存储形式:key--LinkList<value> 首先先给大家Show一波Redis中与List类型相

  • 自己模拟写C++中的String类型实例讲解

    下面是模拟实现字符串的相关功能,它包括一下功能: String(const char * s);//利用字符串来初始化对象 String(); //默认构造函数 String(const String & s);//复制构造函数,利用String类型来初始化对象 ~String(); //析构函数 int length(); //返回String类型中字符串的长度 String & operator=(const String & s);//重载=运算符. String &

  • Redis 中spark参数executor-cores引起的异常解决办法

    Redis 中spark参数executor-cores引起的异常解决办法 报错信息 Unexpected end of stream 16/10/11 16:35:50 WARN TaskSetManager: Lost task 63.0 in stage 3.0 (TID 212, gzns-arch-spark04.gzns.iwm.name): redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end

  • 将java中的 string 类型转成 数组案例

    这个要看你的具体需求了.如果是有分隔符的那种例如"a,b,c";就直接分割就行了. String string = "a,b,c"; String [] stringArr= string.split(","); //注意分隔符是需要转译滴... 如果是"abc"这种字符串,就直接 String string = "abc" ; char [] stringArr = string.toCharArray(

  • C++中的string类型

    目录 1.string 类 1.1 和char *的异同 1.2 C++11初始化 1.3 拼接 1.4 长度 1.5 IO 1.6 原始字符串 1.string 类 1.1 和char *的异同 在C++当中,除了char *类型,还有专门的字符串类型,就叫做string. 通过包含头文件string就可以使用: include<string> 在很多方面,string类型的使用方法和char *一样,例如: string str1; string str2 = "hello wo

  • Redis中缓存穿透/击穿/雪崩问题和解决方法

    目录 缓存问题 1. 缓存穿透---查不到 解决方案 2. 缓存击穿---量太大,缓存过期 解决方案 3. 缓存雪崩 解决方案 缓存问题 1. 缓存穿透---查不到 缓存穿透是指用户想查询一个数据,发现Redis中没有,也就是缓存没有命中,就向持久性数据库发起查询,发现数据库也没有这个数据,于是查询失败了. 当用户请求很多的情况下,缓存没有命中,数据库也没有数据,会给数据库造成很大的压力,这就是缓存穿透. 解决方案 第一种解决方案:使用布隆过滤器 使用布隆过滤器之后,将存储的数据放入布隆过滤器中

  • Redis高并发防止秒杀超卖实战源码解决方案

    目录 1:解决思路 2:添加 redis 常量 3:添加 redis 配置类 4:修改业务层 1:秒杀业务逻辑层 2:添加需要抢购的代金券 3:抢购代金券 5:postman 测试 6:压力测试 8:配置Lua 9:修改业务层 1:抢购代金券 10:压力测试 1:解决思路 将活动写入 redis 中,通过 redis 自减指令扣除库存. 2:添加 redis 常量 commons/constant/RedisKeyConstant.java seckill_vouchers("seckill_v

  • Redis高并发场景下秒杀超卖解决方案(秒杀场景)

    目录 1 什么是秒杀 2 为什么要防止超卖 3 单体架构常规秒杀 3.1 常规减库存代码 3.2 模拟高并发 3.3 超卖现象 3.4 分析原因 4 简单实现悲观乐观锁解决单体架构超卖 4.1 悲观锁 4.2 乐观锁 4.3 redis锁setnx 4.4 使用Redision 5 分布式锁的解决方案 6 采用缓存队列防止超卖 1 什么是秒杀 秒杀最直观的定义:在高并发场景下而下单某一个商品,这个过程就叫秒杀 [秒杀场景] 火车票抢票 双十一限购商品 热度高的明星演唱会门票 … 2 为什么要防止

随机推荐