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://www.cnblogs.com/xyh9039/p/13979522.html
版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!
上一篇: 幽默聊男女,聊到心坎里
下一篇: 这伙爹妈,想把笑友们笑瞎