最近在自己的工作中, 把其中一个 PHP 项目的缓存从以前的 APC 缓存逐渐切换到 Redis 中, 并且根据 Redis 所支持的数据结构做了库存维护功能缓存是在业务层做的, 准确讲应该是在 MVC 模型中 Model 的 ORM 里面主要逻辑就是先查缓存, 查不到的话再查数据库不过这些不是本文的主要内容, 下面我把库存管理功能的缓存设计思路分享一下, 希望能带给大家一些收获, 有不足之处或者有更好方案的, 也希望各位多多指教
一业务背景
为了略去我们公司项目背景, 我决定把这次的问题类比成一个考卷上的问题至于业务细节, 大家也无需关注~ 看题目就可以了:
假设你是某国最牛的收藏家, 手里有各种价值连成的宝物知道有一天, 你觉得做收藏太没意思了, 打算把这些宝物卖掉换点现金
不过把这些值钱的宝贝放在菜市场上卖实在太 low 了在互联网 + 时代, 我们当然要玩一些不一样的卖法: 在你名下有一栋 300 个房间的大楼 (编号为 001 至 300), 每个房间放着一个密码锁保险箱, 在下个月(12 月 1 日至 12 月 31 日) 的每一天, 你都会挑选 300 件最好的极品宝物(也称作 A 类宝物), 分别放入这 300 个房间的保险箱里, 每天每个房间放什么宝物已经定好了, 所有想买宝物的人必须至少提前一天在网上预定, 到时候凭借预定码自己打开保险箱取货没有被预定的宝物将会被你收回, 不再售卖
要做这样一个网络预定系统, 它的前端界面大概是这样的:
上图中三个要填的控件, 单击后可以出现选择框现在的问题是, 一个房间只有一个宝物, 不能被重复预定所以当买家选择了宝物类型和房间号之后, 在选择预定日期时, 要在日期选择框给用户一个提示比如 12 月 3 日 051 号房间已被预定, 现在又有另一位用户选择了 051 号房间, 那么在弹出日期选择框时, 12 月 3 日要置为不可选如下图(12 月 3 日显示为缺):
那么, 这样一个简单的库存系统, 如何在 redis 中存储呢?
二库存管理方案(Redis)
最粗暴的想法是, 我们的库存其实就是一个很大的三维数组, 第一维宝物类型, 第二维房间号, 第三维即预定日期 Redis 支持 5 种存储类型: String,Hash,List,Set,Sorted Set 目前的场景中 Hash 和 Set 类型都可以满足要求, 在此我们选择使用 Hash 类型做存储
Redis 的 key 设置为 宝物类型 + 房间号(例如 A:205,A 代表极品宝物, 205 为房间号),Redis 的 value 为 hash 类型, hash key 为日期(例如 2016-12-05),hash value 为 true 或 false, 表示已经被预定或没有被预定用图表示为:
如果 A 类宝物 158 房间在 12 月 8 日已经被预定, 则存储为
- Redis Key A:158
- Redis Value hash table ['2016-12-08' => 1]
三进阶场景 & 库存管理方案
你所推出的 A 类极品宝物很受欢迎, 刚推出去不久即被预定出去很多然而, 动辄数十万元的价格也让很多有收藏兴趣却没那么富裕的中产阶级望而却步于是, 你又从自己的收藏中挑选出了比 A 类宝物稍次一些的 B 类宝物(也称作优质宝物), 价格更加亲民
由于 B 类宝物比 A 类宝物多一些, 你打算换一种玩法, 在这 300 个房间中, 每个房间又放入了一个保险箱, 这次, 你每隔一个小时都会向 300 个房间的箱中各放入一件 B 类宝物, 没有被预定的宝物在这一个小时过后会被收回, 换成下一个小时的宝物买家预订后, 按照所预定的小时来取走宝物对于 B 类宝物, 你的预定系统会多了一个选项, 即取货时间如下图:
现在由于多了一个预定条件(取货时间), 那在做库存存储的时候, 粗暴的方式想一下, 库存其实就是一个大的四维数组第一维宝物类型, 第二维房间号, 第三维预定日期, 第四维取货时间在 Redis 中怎样存储这类宝物呢?
其实仔细想一下, 在存储 A 类极品宝物的时候, 我们在 Redis 中的存储是有浪费维度的情况的,
当时 hashValue 只存了一个 true 表示有预定, 这个维度其实是被浪费掉了考虑到取货时间全是整点, 一整天也就是 0 至 1 点, 1 至 2 点,,23 至 24 点共计 24 种情况, 所以我们完全可以使用二进制整数表示被预定的时间例如 1 表示 0 至 1 点, 2 表示 1 至 2 点, 4 表示 2 至 3 点,,
8388608 (= 2^23)表示 23 至 24 点多个时间段被预定, 只需要将数值取逻辑或操作即可
这样, 我们的 Redis 结构变成了这样子:
例如, B 类宝物 103 房间, 12 月 5 日和 6 日的上午 8 点至 12 点被预定, 在 redis 中存储为
- Redis Key B:103
- Redis Value 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 个房间中, 每个房间新增了 100 个宝箱, 专门用于存放 C 类宝物这 100 个宝箱分别被编号为 1 号, 2 号,,100 号同样的, 每天的每个小时, 你都会向这 300 个房间中, 每个房间的 100 个宝箱中分别放入一件 C 类宝物 (也就意味着, 整个大楼每小时 C 类宝物会更新 30000 件) 如果没有人预定, 则下一个小时宝物更换终于, 这下可以满足所有人的需求了
对于 C 类宝物, 你的预定界面成了下面的样子:
我们又多了一个预定条件此时, 又面临着库存存储的问题照例, 这个库存其实就是一个大的五维数组, 宝物类型房间号预定日期取货时间宝箱编号各自占有一个维度不过前面我们的 Redis 各个维度基本上已经占满了, 这次应该怎么存储呢?
这次的 Redis 库存存储必须要结合业务特点来了首先, 宝箱编号和取货时间这两个维度, 能取的值范围并不太多, 宝箱编号只有 100 个, 只要把 hash value 变成一个长度为 100 的数组, 数组的每个位置都存有 INT 类型表示的取货时间即可然而 hash value 只能是 string 于是乎, 只好做一个数组的序列化操作, 读取的时候再反序列化回来即可好在长度只有 100, 序列化效率并不会成为系统的瓶颈
例如, C 类宝物, 12 月 23 日 24 日, 258 房间, 97 和 99 号宝箱在 11 点至 13 点被预定, 则存储为:
- Redis Key C:258
- Redis Value hash table ['2016-12-23' => '[97 => 6144, 99 => 6144]', '2016-12-24' => '[97 => 6144, 99 => 6144]' ]
其中 6144 用二进制表示为 110000000000,hash value 为数组序列化以后的字符串, 实际项目中可以使用 json 格式好了, 现在 Redis 对于三种宝物的存储都有了
对于 C 类宝物, 在用户取消预定新增预定时, 同样不能简单地调用 hSet 和 hDel 进行覆盖设置和删除, 要取出已经预定的情况, 与已经预定的取货时间做位运算
五存储优化
库存理论上就是一个多维数组, 我们所做的主要工作就是怎样把各个维度合理的存储起来, 并能够方便地进行增加删除查询操作从节约使用内存的角度讲, 在最开始还没有任何人预定的时候, Redis 整个可以是空的, 对于 A 类宝物来说, hash value 等于 false 和根本不存在对应的 redis key 或 hash key 是等效的
另外, 宝物类型和房间号合起来做 redis key, 会导致我们在 redis 中和宝物库存相关的 key 的数量比较多, 为了方便统一管理这些 key, 可以再增加一条 redis 缓存, 专门用来存储和宝物库存相关的所有 redis key 值, 如下图所示需要注意的是, 这次我们并不需要 hash 数据类型了, set 类型就已经足够, 增删改查复杂度都是 O(1)里面存储了所有 redis 中已经存在的库存 key 值
这么做的一个好处是, 万一哪天碰到一些特殊情况, 需要把所有库存相关缓存全部清空的话, 我们可以很容易地取出所有的库存 key 并做删除操作另外一个好处是, 给我们提供了继续扩展的思路设想一下, 现在最复杂的情况是 C 类宝物, 一共 5 个维度假设未来, 你不再使用一幢楼的 300 个房间去售卖宝物, 而是多幢楼, 那么用户在下订单的时候又要多出一个维度楼栋编号碰到这种情况, 我们完全可以将这个多出来的库存 Key 集合退化为楼栋编号来使用, 保证了可能出现的更复杂情况下的扩展性
在做了这次扩展之后, 每次新增预定记录时, 需要注意检测库存 key 集合中是否已经存在对应的 redis key 值, 如果不存在需要将 redis key 值加入库存 key 集合中删除操作也类似
六总结
上面使用了循序渐进的方法讲述了一下问题, 不过现实的场景中, 这三种宝物类型在我们的业务中是同时存在的上面的设计保持了三种宝物类型存储上的统一性如果只考虑 A 类宝物的话, 库存只有三个维度, 其实完全不必使用 hash 数据类型来存储, set 类型就足够了
我们存储这些预定情况的主要目的, 就是为了方便快速地查到库存冲突情况比如有人已经定了 12 月 3 日, 59 号房间的 A 类宝物, 那又有另外一个人想预定一样的日期房间的 A 类宝物时, 通过内存中的库存查询, 我们可以很方便地告诉客户, 该库存已经其他人抢先预定了
以上就是我在业务中碰到的一个缓存设计的小问题, 不吝赐教!
来源: http://www.linuxidc.com/Linux/2018-03/151123.htm