zoukankan      html  css  js  c++  java
  • Redis分布式缓存系列(二)- Redis中的String类型以及使用Redis解决订单秒杀超卖问题

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

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

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

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

    对于.Net而言,Redis操作一般使用ServiceStack.Redis 或者 StackExchange.Redis ,但总体来说ServiceStack.Redis性能更优。

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

    ServiceStack.Redis源码GitHub地址:https://github.com/ServiceStack/ServiceStack.Redis

    一、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
            }
        }
    }

    其中Redis连接字符串(Redis Connection Strings)支持以下几种格式:

    localhost
    127.0.0.1:6379
    redis://localhost:6379
    password@localhost:6379
    clientid:password@localhost:6379
    redis://clientid:password@localhost:6380?ssl=true&db=1

    下面直接给大家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小时6000次请求限制--可破解)
        /// </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/1B_XUM4Eqc81CJdjufOWS9A 
    提取码:a78n

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

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

  • 相关阅读:
    Linux-nmap
    MongoDb注意事项
    HTML如何转XTML
    Centos 64位 Install certificate on apache 即走https协议
    CentOS 下搭建部署独立SVN服务器全程详解(5.5)
    LNMP安装与配置
    64位CentOS 6.0下搭建LAMP环境
    Apache遇到的问题:APR not found
    超详细LAMP环境搭建
    偏方治百病
  • 原文地址:https://www.cnblogs.com/xyh9039/p/13979522.html
Copyright © 2011-2022 走看看