一 背景
这里我讲一个小故事,和我们实际的场景是一样的,比如有一家名叫大A的包子铺,包子铺有一个点餐系统,假如用户A使用点餐系统下单,点了一份韭菜馅的包子后,会给它一个号,如38号,等到38号时会叫他取餐,但是这个包子铺的点餐系统有点不同,它为了更方便的割韭菜,它开通了会员卡制度,比如会员有三个等级:普通用户、vip、svip。那么会员卡有什么用呢?可以超前点餐插队。如图所示:
现在已有三个普通用户在排队等待:
这时,来了一个vip用户D,队列会变成这样:
然后来了依次来svip用户E和vip用户F后,队列如下:
二 确定需求
首先我们先确定一下需要哪些方法, 再来想具体的解决方案,在我看来主要需要以下几个方法:
- 添加:就是用户插队的时候插入到哪里。
- 获取当前用户前面还有多少人排队:每个用户都非常关心前面还有多少人,所以它们会一直轮询问:"我前面还有多少人?"。
- 删除:有用户嫌前面等的人太多了而中途离开了。
- 获取队头元素并删除:相当于厨师查看点餐系统,拿到需要处理的套餐,在我们上面的例子就是,获取svip用户E点的餐。
三 解决方案
解决方式一:zset
zset为有序的,自动去重的集合数据类型。
增加
使用订单号作为key插入,订单号是趋势递增的,订单号一般都是用雪花算法,雪花算法第1位默认是没有使用的,这里我们用作等级即可,如svip:1、vip:2、普通用户:3。
获取当前用户前面还有多少人排队
使用ZRANK命令获取前面还有多少人排队。
删除
直接删除对应的元素。
获取队头元素并删除
获取并删除队头元素。
解决方式二:基于redis自实现
既然有了zset,为什么还要自己实现呢?因为zset使用跳表实现,插入和删除的时间复杂度是O(logn),我在想能不能自己实现一个O(1)的呢?
增加
初始时就创建3个队列,每次添加时,添加到对应等级队列的末尾,时间复杂度为O(1)。
获取当前用户前面还有多少人排队
需要每个队列实现一个计数器count,利用Map存储每个元素的计数值,如图所示:
那怎么计算前面有多少用户呢?
当前用户前面有多少人排队=前面所有队列的长度和+(当前用户的count-当前用户所在队列的头元素的count)
比如计算F前面排了多少人=svip长度(1)+F的计数(2)-所在队列头元素的计数也就是D(1)=2,也就是它前面还有两个人排队。同理B呢? = (1 + 2) +(2 -1)=4,前面有四个人排队,也就是E、D、F、A 。时间复杂度也是O(1)。
其实理论来说是:队列的数量O(M),后续我会优化成一个真正的O(1)。 后续我做了优化可以跳转👉juejin.cn/post/747256…
删除
删除怎么实现O(1)呢?其实这个我想了很久,如果直接删除,就需要遍历队列找到元素的位置,时间复杂度为O(N),即便是有序,使用二分查找也是O(logN),所以我采用了惰性删除,也就是说不是真的删除,而只是标记了删除,下次读取的时候跳过。如图所示假如我删除D:
直接将countMap的元素删除掉即可,下次读取时如果countMap没有,则直接跳过。但是获取当前用户前面还有多少人排队这个方法就不准确了,不过我们的场景只需要一个大概值就可以。
获取队头元素并删除
比较简单,先从svip队列中删除,检查在countMap中是否存在,如果不存在则接着读取,svip为空就读vip。
四、总结
先说一下自实现的优缺点:
- 优点:时间复杂度均为O(1)
- 缺点:实现较为复杂,而且获取当前用户前面还有多少人排队也不精准。
最后我在github上根据这些思路写了一个类库,因刚学go语言不久,所以代码写的比较粗糙,大佬们有时间可以帮我修改下,不胜感激。🔗github地址