優化迴圈 Query DB,如何提前確保 generated token unique

建立一筆訂單時,我們會給予一串 serial number,作為訂單唯一識別。我們會寫出如下的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/models/model_name.rb
class ModelName < ActiveRecord::Base
include Tokenable
end

# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern

included do
before_create :generate_token
end

protected

def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless self.class.exists?(token: random_token)
end
end
end

Query 是使用搜尋效益較高的 exists

1
User Exists? (0.8ms)  SELECT 1 AS one FROM `users` WHERE `users`.`status` = 0 LIMIT 1

比用 where 或 find_by 好

1
User Load (0.9ms)  SELECT `users`.* FROM `users` WHERE `users`.`status` = 0 LIMIT 1

基本的阻擋會在 DB column 上 UNIQUE,在 model 層 validates 做 uniqueness

且 token 上索引(index)那效能大部分沒什麼問題

而假設商業邏輯需要複雜的運算,導致在 new 和 save 之間需要 lock tables 等

而 lock 很多 tables,或者大型 tables 時

我們需要避免鎖太久,以及高併發導致 dead locked 之類

這時候還想要做效能優化,想到是提前一步確定 「token 唯一性」

看到滿不錯的方法,筆記一下

核心使用 $redis.incr(key) 特性

採取不嚴謹的準確性,來換取時間

其實是一種 Tradeoff 跟我之前文章提到的「布隆過濾器」類似

不管空間,降低時間消耗

使用 $redis.incr(key) 特性
先取得一組數組(111),直接往 redis 塞拼出來的 Hash Key

1
2
3
4
5
6
[13] pry(main)> $redis.incr("Gmi_token:111")
=> 1
[14] pry(main)> $redis.incr("Gmi_token:111")
=> 2
[15] pry(main)> $redis.incr("Gmi_token:111")
=> 3

只第一次的 key 值,會得到 1,重複就會累加

這樣就可以在 lock tables 之前,先驗證 是否產生過?

衍生問題一: 可能製造一堆沒被訂單使用的 Hash key 在 redis

透過 $redis.expire(key, EXPIRE_TIME)
2 - 3 天內過期掉已經 generated Hash key

衍生問題二: 既然會過期舊的,How to ensure generated token with hash key would be unique?

透過 timestamp.strftime('%Y%m%d') 拼上一組時間戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module RedisUnique
EXPIRE_TIME = 86400 * 3

def self.check(full_serial)
date_flag = timestamp.strftime('%Y%m%d')
return unique_key?("SERIAL_FLAG:#{date_flag}:#{full_serial}")
end

private

def unique_key?(key)
result = $redis.incr(key) == 1
$redis.expire(key, EXPIRE_TIME)
return result
end
end

這樣就可以透過下方調用,做到用 redis 空間去換時間

並取得唯一數值,不怕鎖表太久等問題

1
RedisUnique.check("20210427H118709")

這是我個人淺見,算是易懂的設計

有任何更好得想法、做法,歡迎底下留言討論喔!

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×