怎么用Redis做预定库存缓存功能

一、业务背景

我决定将本次问题比作考卷上的问题,以避免介绍我们公司项目的背景。至于业务细节,大家也无需关注~看题目就可以了:

假设你是某国最牛的收藏家,手里有各种价值连成的宝物。有一天你可能会感到收藏变得无聊了,于是决定出售这些珍贵物品以获取现金。

Redis实战如何使用Redis实现预定库存缓存功能

不过把这些值钱的宝贝放在菜市场上卖实在太low了。在“互联网+”时代,我们当然要玩一些不一样的卖法:在你名下有一栋300个房间的大楼(编号为001至300),每个房间放着一个密码锁保险箱,在下个月(12月1日至12月31日)的每一天,你都会挑选300件最好的“极品宝物”(也称作A类宝物),分别放入这300个房间的保险箱里,每天每个房间放什么宝物已经定好了,所有想买宝物的人必须至少提前一天在网上预定,到时候凭借预定码自己打开保险箱取货。没有被预定的宝物将会被你收回,不再售卖。

要做这样一个网络预定系统,它的前端界面大概是这样的:

上图中三个要填的控件,单击后可以出现选择框。现在的问题是,一个房间只有一个宝物,不能被重复预定。当买家选定宝物类型和房间号后,当他们选择预定日期时,建议在日期选择框里提供一些提示信息。比如12月3日051号房间已被预定,现在又有另一位用户选择了051号房间,那么在弹出日期选择框时,12月3日要置为不可选。如下图(12月3日显示为“缺”):

那么,这样一个简单的库存系统,如何在redis中存储呢?

二、库存管理方案(Redis)

我们最初的构想是,我们的存货可以被看作是一个巨大的三维数组,其中第一维表示宝物类型,第二维表示房间号,第三维表示预定日期。Redis提供了五种存储类型,分别为:String、Hash、List、Set、Sorted Set。我们可以在当前场景下使用Hash类型来存储数据,因为它能够满足我们的需求,同时Set类型也是可行的选项。

Redis的key设置为 宝物类型+房间号(例如 A:205,A代表极品宝物,205为房间号),Redis的value为hash类型,hash key为日期(例如 2016-12-05),hash value为true或false,表示已经被预定或没有被预定。用图表示为:

如果A类宝物158房间在12月8日已经被预定,则存储为

1

2

3

Redis Key &
mdash;
&
mdash;
A:158

Redis Value &
mdash;
&
mdash;
hash table ['
2016-12-08'
=>
1]

三、进阶场景&
库存管理方案

A类顶级宝物的推出受到了热烈欢迎,仅推出不久便已被订购数不少。许多中产阶级对收藏感兴趣,但高昂的价格常常令他们望而却步。于是,你从自己的珍藏中选择了B类宝物,它比A类宝物稍逊一些,但价格更为合理,也被称为“优良宝物”。

由于B类宝物比A类宝物多一些,你打算换一种玩法,在这300个房间中,每个房间又放入了一个保险箱,这次,你每隔一个小时都会向300个房间的箱中各放入一件B类宝物,没有被预定的宝物在这一个小时过后会被收回,换成下一个小时的宝物。买家预订后,按照所预定的小时来取走宝物。对于B类宝物,你的预定系统会多了一个选项,即取货时间。如下图:

现在由于多了一个预定条件(取货时间),那在做库存存储的时候,粗暴的方式想一下,库存其实就是一个大的四维数组。该句话可以重写为:四维信息包括宝物类型、房间号、预定日期和取货时间。在Redis中怎样存储这类宝物呢?

其实仔细想一下,在存储A类极品宝物的时候,我们在Redis中的存储是有浪费维度的情况的,

实际上,当时只使用了一个hashValue存储了预定的状态,导致该维度的信息被浪费了。考虑到取货时间全是整点,一整天也就是0至1点,1至2点,&
hellip;
&
hellip;
,23至24点共计24种情况,所以我们完全可以使用二进制整数表示被预定的时间。例如1表示0至1点,2表示1至2点,4表示2至3点,&
hellip;
&
hellip;

23至24点可以用2的23次方(8388608)来表示。多个时间段被预定,只需要将数值取逻辑或操作即可。

这样,我们的Redis结构变成了这样子:

例如,B类宝物103房间,12月5日和6日的上午8点至12点被预定,在redis中存储为

1

2

3

Redis Key &
mdash;
&
mdash;
B:103

Redis Value &
mdash;
&
mdash;
hash table ['
2016-12-05'
=>
3840, '
2016-12-06'
=>
3840]

对于B类宝物,在做新增预定时,需要注意先将原有的hash value取出,和新的预定取货时间做逻辑或操作,然后再把结果写回Redis中,而不能像A类宝物一样直接调用hSet去设置hash value;取消预定时,要注意先将原有的hash value取出,把要取消的时间段从hash value中扣除掉(异或+逻辑与操作),然后重新将剩余的已预订取货时间写回Redis中,而不能直接调用hDel去删除。

四、再次进阶&
库存管理方案

自从推出了B类宝物之后,你的生意又比以往火爆了许多。于是新的需求又来了,现在有大量的游客、学生党等没什么丰厚积蓄的人表示对你的宝物非常感兴趣,来这个城市旅游的人都希望带一些纪念品回去。尽管B类宝物价格略低于A类,但对于这些人来说仍有些昂贵。于是,你决定把自己余量最多的实惠宝物(C类宝物)拿出来售卖。

在这300个房间中,C类宝物储存数量最多,因此你在每个房间增加了100个专门用于储存C类宝物的宝箱。这100个宝箱分别被编号为1号,2号,&
hellip;
&
hellip;
,100号。同样的,每天的每个小时,你都会向这300个房间中,每个房间的100个宝箱中分别放入一件C类宝物(也就意味着,整个大楼每小时C类宝物会更新30000件)。如果没有人预定,则下一个小时宝物更换。终于,这下可以满足所有人的需求了。

对于C类宝物,你的预定界面成了下面的样子:

我们又多了一个预定条件。此时,又面临着库存存储的问题。照例,这个库存其实就是一个大的五维数组,宝物类型、房间号、预定日期、取货时间、宝箱编号各自占有一个维度。前面我们已经用掉了Redis的各个容量,现在要存储数据该怎么办?

这次的Redis库存存储必须要结合业务特点来了。首先,宝箱编号和取货时间这两个维度,能取的值范围并不太多,宝箱编号只有100个,只要把hash value变成一个长度为100的数组,数组的每个位置都存有INT类型表示的取货时间即可。然而hash value只能是string&
hellip;
&
hellip;
于是乎,只好做一个数组的序列化操作,读取的时候再反序列化回来即可。好在长度只有100,序列化效率并不会成为系统的瓶颈。

存储方式为:12月23日、24日在258房间的C类宝物中,编号为97和99的宝箱在上午11点至下午1点期间已被预定

1

2

3

Redis Key &
mdash;
&
mdash;
C:258

Redis Value &
mdash;
&
mdash;
hash table ['
2016-12-23'
=>
'
[97 =>
6144, 99 =>
6144]'
, '
2016-12-24'
=>
'
[97 =>
6144, 99 =>
6144]'
]

其中6144用二进制表示为&
lsquo;
110000000000&
rsquo;
,hash value为数组序列化以后的字符串,实际项目中可以使用json格式。好了,现在Redis对于三种宝物的存储都有了。

对于C类宝物,在用户取消预定、新增预定时,同样不能简单地调用hSet和hDel进行覆盖设置和删除,要取出已经预定的情况,与已经预定的取货时间做位运算。

五、存储优化

库存理论上就是一个多维数组,我们所做的主要工作就是怎样把各个维度合理的存储起来,并能够方便地进行增加、删除、查询操作。从节约使用内存的角度讲,在最开始还没有任何人预定的时候,Redis整个可以是空的,对于A类宝物来说,hash value等于false和根本不存在对应的redis key或hash key是等效的。

另外,宝物类型和房间号合起来做redis key,会导致我们在redis中和宝物库存相关的key的数量比较多,为了方便统一管理这些key,可以再增加一条redis缓存,专门用来存储和宝物库存相关的所有redis key值,如下图所示。需要注意的是,在这种情况下,使用set数据类型就可以满足要求了,而不必使用hash数据类型,因为set数据类型的增删改查复杂度都为O(1)。里面存储了所有redis中已经存在的库存key值。

这么做的一个好处是,万一哪天碰到一些特殊情况,需要把所有库存相关缓存全部清空的话,我们可以很容易地取出所有的库存key并做删除操作。另外一个好处是,给我们提供了继续扩展的思路&
hellip;
&
hellip;
设想一下,现在最复杂的情况是C类宝物,一共5个维度。假设未来,你不再使用一幢楼的300个房间去售卖宝物,而是多幢楼,那么用户在下订单的时候又要多出一个维度&
mdash;
&
mdash;
楼栋编号。碰到这种情况,我们完全可以将这个多出来的库存Key集合退化为楼栋编号来使用,保证了可能出现的更复杂情况下的扩展性。

在做了这次扩展之后,每次新增预定记录时,需要注意检测库存key集合中是否已经存在对应的redis key值,如果不存在需要将redis key值加入库存key集合中。删除操作也类似。



预定库存是一种比较常见的电商场景,但同时也是一个比较复杂的问题。为了保证订单可以成功下单,需要在下单前将商品库存进行冻结,这个过程需要考虑各种并发和容错问题。在本篇文章中,我们将介绍如何使用Redis实现预定库存的缓存功能,以提高系统的性能和稳定性。
一、预定库存的背景
在电商场景下,预定库存是一种很常见的需求。以秒杀为例,用户在秒杀过程中需要占据商品库存以保证自己的购买成功。一般情况下,电商系统会在用户下单前锁定一定数量的库存,如果用户下单成功,则实际库存数量减去锁定数量,否则库存数量不变。
二、预定库存的实现原理
实现预定库存的核心原理是将库存的数量通过一定规则锁定,直到用户购买成功或者有超时机制。在锁定的过程中,需要考虑各种并发和错误情况。一般情况下,我们会采用Redis来实现预定库存的缓存功能。具体实现步骤如下:
1. 对于每个商品库存,创建一个对应的key,比如\"product:001:stock\"。
2. 用户在下单前,通过Redis的INCRBY命令将库存的数量减去预定的数量,比如\"DEC:product:001:stock\"。
3. 如果减去后的库存数量小于0,则表示库存不足,下单失败。需要通过Redis的INCRBY命令将减去的数量加回来。
4. 如果下单成功,则需要将预定库存的信息保存到Redis中,比如使用Hash表来保存用户ID和预定数量的对应关系,比如\"HSET:product:001:booking user1 2\"。
5. 如果下单失败,则需要将已经减去的库存数量加回来,比如\"INCRBY:product:001:stock 2\"。
6. 如果用户在规定时间内下单成功,则需要将预定库存从Redis中删除,比如\"HDEL:product:001:booking user1\"。
三、预定库存的应用场景
预定库存的应用场景比较广泛,除了电商场景外,还包括机票、酒店、景点等其他购买场景。在这些场景下,预定库存方案可以有效保证订单的稳定性和系统的可用性。
四、预定库存的缓存优化
实现预定库存的过程中,需要考虑到并发和容错问题,以提高系统的稳定性和性能。针对这些问题,我们可以通过以下方式进行优化:
1. 使用Redis作为预定库存的缓存,可以在性能和稳定性上提高很多。
2. 对于每个商品库存,可以使用分布式锁来保证并发操作的一致性。
3. 在预定库存的过程中,需要考虑到超时机制和冲突检测,以避免潜在的错误情况。
4. 使用Redis的管道功能,可以批量处理一些操作,以提高系统的性能和吞吐量。
五、预定库存的安全风险
预定库存虽然在提高系统性能和稳定性上有很多优势,但同时也存在一些安全风险。主要包括如下几个方面:
1. 如果没有设置好超时机制,可能导致库存一直被锁定,从而影响系统的正常运行。
2. 如果存在并发操作,可能导致库存出现负数情况,从而导致订单或者系统的不稳定。
3. 如果没有设置好规则或者容错机制,可能会出现库存死锁等问题,从而影响用户购买体验和网站品牌形象。
六、总结
预定库存是电商和其他购买场景下非常常见的问题,其缓存优化和技巧也备受关注。使用Redis作为预定库存的缓存,可以提高系统性能和稳定性,避免并发冲突和超时等问题。但同时也需要注意安全风险,及时进行规则和容错机制的优化。