假設我們手上有這么一個需求:
* 接受用戶的訂單數據,但因為訂單處理需要一定的時間,所以只能后臺先保存訂單數據,對用戶進行排隊操作。當然這個排隊操作,用戶是不透明的,某些用戶的請求可能被優先處理。
* 用戶很關心自己訂單目前的處理進度,即類似去銀行排隊拿號的時候,小票上顯示“你前面還有多少人在排隊”。所以后臺要能告知用戶目前他的訂單進度。
* 能給用戶或者產品經理顯示目前正在排隊的訂單數有多少。這樣才能讓用戶知道我們的業務多么的有市場。
## 一、需求分析
* 需求1可以理解為寫操作,需要后端存儲數據。
* 需求2,需求3可以理解為讀操作,而且可以預見這個讀操作應該比寫操作頻繁。如果用戶很關注她的訂單進展,說不定會一直刷新查看他的訂單排隊情況。
## 二、實現方案 — MySQL
用戶的訂單數據肯定得持久化存儲,MySQL是一個不錯的選擇。既然需求這么簡單,無非一個訂單數據嘛。暫且用一張表“訂單表(T\_Order)”來保存正在排隊的訂單,已經處理完畢的訂單則從T\_Order表遷移至“(歷史訂單表T\_History\_Order)”。這樣的好處避免訂單表數據量太大,提高讀寫性能。
1.需求1完成訂單的入庫,顯然就是一個insert語句了,類似:
~~~
insert into T_Order(...) values(...);
~~~
2.需求2查詢自己訂單的排隊情況,那肯定看比自己訂單時間還早的用戶有多少人了。這些比自己下單時間還早的人,就是排在自己前面的人了。假設一個用戶同時只能有一個訂單在排隊。
~~~
#先查出自己訂單時間, 假設是1429389316
select orderTime from T_Order where uid=8888;
#再查有多少人的訂單時間比自己的早
select count(orderTime) from T_Order where orderTime <= 1429389316;
#先查出自己訂單時間, 假設是1429389316
select orderTime from T_Order where uid=8888;
#再查有多少人的訂單時間比自己的早
select count(orderTime) from T_Order where orderTime <= 1429389316;
~~~
3.需求3查詢所有的訂單數,顯然就是一個select語句了,類似:
~~~
select count(1) from T_Order;
~~~
## 三、遇到問題
互聯網的精髓就是“小步快跑,快速迭代”。用MySQL快速完成需求,面向用戶服務后。初期階段,一切ok。但是當這個業務運營得好,用戶量大的時候,就會發現用戶經常投訴“我查詢自己的訂單排隊進度,經常報錯”。甚至處理訂單的同事,也經常抱怨從訂單系統里面查看訂單,非常緩慢。select count 操作基本都是全表掃描操作,看來MySQL面對這么大規模的全表查詢操作,還是有點吃力。
## 四、解決查詢性能問題
NoSQL在互聯網領域的江湖地位已經很牢靠了,看來得請他老人家出來救場了。沒錯,使用Redis的有序集合(sorted sets)數據結構,就可以完美的解決這個問題。因為有序集合底層的實現是跳表這種數據結構,時間復雜度是logN,即使有序集合里面的訂單有100萬之多,耗時也基本都是納秒級別(基本不到1毫秒)。
【注意】有序集合有另外一種底層實現—-壓縮列表。不過要求是,有序集合的元素個數小于128。這對于一個龐大的訂單后臺,元素個數絕對不僅僅128。
假設我們在的有序集合名字就叫做:task,那么:
1、用戶提交一個訂單,除了持久化到MySQL之外,我們再異步的寫入redis中。比如用戶uid=8888在1429389316這個時間點提交了一個訂單,那么就往redis有序集合里面添加這個訂單。
~~~
127.0.0.1:6379> zadd task 1429389316 8888
(integer) 1
~~~
2、用戶要查詢自己的訂單排隊情況,這時候我們只要查詢redis的有序集合就可以了。命令為rank:
(為了方便演示,在8888用戶之前,我們假設8885,8886,8887這3個用戶已經提交了訂單,而8889是在8888后提交的訂單。)
~~~
127.0.0.1:6379> zadd task 1429389316 8888
(integer) 1
127.0.0.1:6379> zadd task 1429389310 8887
(integer) 1
127.0.0.1:6379> zadd task 1429389309 8886
(integer) 1
127.0.0.1:6379> zadd task 1429389308 8885
(integer) 1
127.0.0.1:6379> zadd task 1429389326 8889
(integer) 1
127.0.0.1:6379> zrank task 8888
(integer) 3
127.0.0.1:6379>
~~~
可見,僅僅通過一個rank命令就可以準確的反映出用戶訂單的排隊情況。時間復雜度為logN。
3、查詢目前有多少正在排隊的訂單。這個就更簡單了,直接一個zcard命令就可以了。而且時間復雜度是O(1),因為redis的跳表本身已經存儲有這個數據了,直接取出即可!
~~~
127.0.0.1:6379> zcard task
(integer) 5
127.0.0.1:6379>
~~~
4、當這個訂單被處理完成后,直接一個zrem命令將訂單從有序集合中刪除即可。時間復雜度為logN。
~~~
127.0.0.1:6379> zrange task 0 -1
1) "8885"
2) "8886"
3) "8887"
4) "8888"
5) "8889"
127.0.0.1:6379> zrem task 8888
(integer) 1
127.0.0.1:6379> zrange task 0 -1
1) "8885"
2) "8886"
3) "8887"
4) "8889"
127.0.0.1:6379>
~~~
因為Redis基本都是內存操作,而且有序集合的底層實現是跳表這種效率媲美平衡樹,但是實現又簡單的數據結構,從而完美的釋放了MySQL的讀壓力。
五、redis之跳表
既然redis的有序集合這么給力的解決了我們的問題,那我們很有必要記住redis為我們做了什么,簡單分析一下redis是如何通過跳表這種數據結構,通過zadd,zrank,zcard,zrem來滿足我們的需求的!
作者:nickbi
鏈接:https://www.jianshu.com/p/2d5cf0a6da1b
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。