需求
有个生成订单号的需求,对于生成订单号有如下要求
- 不能被猜出订单量
- 唯一性
- 趋势递增
- 订单号包含时间信息
- 防止race condition生成重复的id
- 防止时间回拨生成重复的id
- 满足每秒可以生成1w个订单号
- 订单号不能过长
分析
- 不能被猜出订单量,所以自增id排除掉
- 要求趋势递增,所以uuid排除掉
- 订单号不能过长,所以snowflake、Mongdb objectID之类的排除掉
方案
订单号=12位时间信息+4位随机数
。时间信息精确到秒,加上4位随机数,支持每秒最多生成1w个订单号。
利用redis来实现方案。
incr命令
防止4位随机数重复。初始化的时候就设置一个随机数,而不是从0开始incr。类型TCP协议的ISN(Initial Sequence Number)
time命令
获取redis服务器的时间,防止各机器本地时间不一致,导致订单号重复。
lua脚本
确保原子性,避免race condition.
set命令
指定nx参数,避免race condition.
由于redis用的是AWS的服务,所以高可用性可以保障。
伪代码如下
def generate_order_id():
conn = get_redis_connection("default")
# 获取redis的时间,保证时间递增有序, 而不依赖应用服务器本地时间
# 防止redis服务器时钟出现问题, 比如ntp时间同步,时间变为过去的时间而导致订单号重复
LUA_SCRIPT = """
local ts = redis.call('time')[1]
local order_id_last_timestamp = redis.call('get', 'order_id_last_timestamp')
if order_id_last_timestamp and ts < order_id_last_timestamp then
return 0
end
redis.replicate_commands()
redis.call('set', 'order_id_last_timestamp', ts)
return ts
"""
ts = int(conn.register_script(LUA_SCRIPT)())
if ts == 0:
raise Exception('redis服务器的时间出现问题')
datetime_s = datetime.datetime.fromtimestamp(ts).strftime('%y%m%d%H%M%S')
key_name = 'order_id_%s' % datetime_s # 为了防止incr被用户推测出订单数据量,每秒一个key
salt = random.randint(0, 99999999) % 10000 # 取模取后面4位随机数;初始化的时候就设置的随机数,而不是每次都从0000开始,防止用户猜测
if conn.set(key_name, salt, ex=10, nx=True): # nx表示如果key不存在才set进去, 防止刚好其它订单同时设置
return '%s%04d' % (datetime_s, salt) # '%04d'使不足4位的数字前面补0
salt = conn.incr(key_name) % 10000
return '%s%04d' % (datetime_s, salt)
因为lua脚本写入非确定的值如time命令,会导致主从同步出问题。比如主库执行一次写入time命令的值,从库也执行一次写入time命令的值,2次time命令的值不一样,这样主从就不一致了。
所以默认情况下,会报Write commands not allowed after non deterministic commands错误.
解决方法是使用redis3.2以上的新特性redis.replicate_commands()
。