Redis分佈式緩存系列(二)- Redis中的String類型以及使用Redis解決訂單秒殺超賣問題

本系列將和你們分享Redis分佈式緩存,本章主要簡單介紹下Redis中的String類型,以及如何使用Redis解決訂單秒殺超賣問題。html

Redis中5種數據結構之String類型:key-value的緩存,支持過時,value不超過512M。redis

Redis是單線程的,好比SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy等等,這些看上去像是組合命令,但其實是一個具體的命令,是一個原子性的命令,不可能出現中間狀態,能夠應對一些併發狀況。下面咱們直接經過代碼來看下具體使用。數據庫

首先來看下Demo的項目結構:緩存

對於.Net而言,Redis操做通常使用ServiceStack.Redis 或者 StackExchange.Redis ,但整體來講ServiceStack.Redis性能更優。安全

此處推薦使用的是ServiceStack包,雖然它是收費的,有每小時6000次請求限制,可是它是開源的,能夠將它的源碼下載下來破解後使用,網上應該有挺多相關資料,有興趣的能夠去了解一波。服務器

1、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小時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類型在項目中使用是最多的,想必你們都有所瞭解,此處就再也不作過多的描述了。

2、使用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解決訂單秒殺超賣問題
///     超賣:訂單數超過商品
///     一、Redis原子性操做--保證一個數值只出現一次--防止一個商品賣給多我的
///     二、用上了Redis,一方面保證絕對不會超賣,另外一方面沒有效率影響,還有撤單的時候增長庫存,能夠繼續秒殺,
///        限制秒殺的庫存是放在redis,不是數據庫,不會形成數據的不一致性
///     三、Redis可以攔截無效的請求,若是沒有這一層,全部的請求壓力都到數據庫
///     四、緩存擊穿/穿透---緩存down掉,請求所有到數據庫
///     五、緩存預熱功能---緩存重啓,數據丟失,多了一個初始化緩存數據動做(寫代碼去把數據讀出來放入緩存)
/// </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/1v6vGIBeIPUlTR9ASjXhcfA 
提取碼:mjam

此文由博主精心撰寫轉載請保留此原文連接:https://www.cnblogs.com/xyh9039/p/13979522.html

相關文章
相關標籤/搜索