<commit_before>---
layout: post
title: 如何保证生成订单号的唯一性
category : 其他技术问题
tags : [订单号, 唯一性, 电子商务]
---
{% include JB/setup %}

如何保证生成订单号的唯一性?之前面试的时候被问到这个问题.我首先想到web安全里面的session安全问题.一个session必须要值空间足够大,这样才不会被暴力破解.
生成session的方法如下:

1, 在session前段加入足够的熵源, 比如加入时间戳和用户的ip+user-agent信息.当然,这段信息可以用哈希函数加截断压缩至想要的长度.

2, 在上一步的基础上连接几位随机数字.

当然这跟唯一性不是一回事.但上面的方法可以很好地减少冲突的发生.但是如何保证绝对的唯一性呢.我想到几个个算比较简单的方法:<!-- excerpt -->

### 方法一: 通过mysql unique key来保证唯一性

如果使用mysql来存储订单信息,那么我们可以将订单号字段设为唯一索引,通过mysql的唯一索引的性质来保证订单号唯一性.具体做法是,在订单号字段上加上unique key,使用上诉类似session生成的方法生成一个新订单号,insert 订单,如果mysql报 "duplicate key" error,那么重生生成订单号并重试insert,直到保存成功为止.我相信通过上面生成订单的方式insert失败是很少发生的.因为同一时间同一公网ip(这里其实还可以精确到内网主机,具体做法参见[如何获取内网ip](/%E5%85%B6%E4%BB%96%E6%8A%80%E6%9C%AF%E9%97%AE%E9%A2%98/2017/11/17/%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E7%94%9F%E6%88%90%E8%AE%A2%E5%8D%95%E5%8F%B7%E7%9A%84%E5%94%AF%E4%B8%80%E6%80%A7 "如何获取内网ip"))内生成的订单量还是很少的.使用唯一索引,mysql会先检查这个字段是否会导致重复.我没有看过mysql源码,但我猜mysql会锁住欲插入的记录所在的那个数据块(B+tree的一个叶节点的所有索引值).

### 方法二: 通过mysql自增列

使用这种方法,将订单列设为整型并自增,让mysql来生成订单号.但这种方法是让所有的新订单去竞争 AUTO_INCREMENT 锁.但好在mysql5.1.22以后可以设置innodb_autoinc_lock_mode的值来选择 AUTO_INCREMENT 锁方式.默认是产生一个轻量级锁,在内存中用互斥量来维护一个计数器.

### 方法三: 通过redis的自增原子操作

redis可以对整型数据进行原子性的自增操作,并返回增加后的值,这保证了几个进程同时进行增加操作时,增量是完全的.即不会出现n个进程对同一个 key 进行 incr(key,1) 操作后,增量小于 n 的现象.

还可以对订单进行分布式生成.具体做法是,将redis主机ID(如果是集群redis)+订单时间戳(精确到秒)+substr(md5(用户ip+user-agent))作为一个key.对属于此范围的订单进行incr操作,生成序号number,作为后缀.最后将key+number作为订单号.

或者再简单点,主机ID+时间戳+number .主机ID可以区分不同主机相同序号的订单.时间戳区分同一主机redis崩溃后恢复,内存被清除,重新计数导致的相同序号的订单(但要保证1s后再重启).如果可以恢复崩溃前的数据,则可以省掉时间戳.

这样的订单号是绝不会重复的.而且生成速度很快.这种方法和方法二类似,都是通过维护计数器.不同的是,方法二的计数器是全局的,而这种方法的计数器只对某个范围的订单计数.这样在mysql的订单号列可以不用加 unique key.
<commit_msg>Update 2017-11-17-如何保证生成订单号的唯一性.md<commit_after>---
layout: post
title: 如何保证生成订单号的唯一性
category : 其他技术问题
tags : [订单号, 唯一性, 电子商务]
---
{% include JB/setup %}

如何保证生成订单号的唯一性?之前面试的时候被问到这个问题.我首先想到web安全里面的session安全问题.一个session必须要值空间足够大,这样才不会被暴力破解.
生成session的方法如下:

1, 在session前段加入足够的熵源, 比如加入时间戳和用户的ip+user-agent信息.当然,这段信息可以用哈希函数加截断压缩至想要的长度.

2, 在上一步的基础上连接几位随机数字.

当然这跟唯一性不是一回事.但上面的方法可以很好地减少冲突的发生.但是如何保证绝对的唯一性呢.我想到几个个算比较简单的方法:<!-- excerpt -->

### 方法一: 通过mysql unique key来保证唯一性

如果使用mysql来存储订单信息,那么我们可以将订单号字段设为唯一索引,通过mysql的唯一索引的性质来保证订单号唯一性.具体做法是,在订单号字段上加上unique key,使用上诉类似session生成的方法生成一个新订单号,insert 订单,如果mysql报 "duplicate key" error,那么重生生成订单号并重试insert,直到保存成功为止.我相信通过上面生成订单的方式insert失败是很少发生的.因为同一时间同一公网ip(这里其实还可以精确到内网主机,具体做法参见[如何获取内网ip](/2017/11/19/%E5%A6%82%E6%9E%9C%E8%8E%B7%E5%8F%96%E8%AE%BF%E9%97%AE%E8%80%85%E7%9A%84%E5%86%85%E7%BD%91ip "如何获取内网ip"))内生成的订单量还是很少的.使用唯一索引,mysql会先检查这个字段是否会导致重复.我没有看过mysql源码,但我猜mysql会锁住欲插入的记录所在的那个数据块(B+tree的一个叶节点的所有索引值).

### 方法二: 通过mysql自增列

使用这种方法,将订单列设为整型并自增,让mysql来生成订单号.但这种方法是让所有的新订单去竞争 AUTO_INCREMENT 锁.但好在mysql5.1.22以后可以设置innodb_autoinc_lock_mode的值来选择 AUTO_INCREMENT 锁方式.默认是产生一个轻量级锁,在内存中用互斥量来维护一个计数器.

### 方法三: 通过redis的自增原子操作

redis可以对整型数据进行原子性的自增操作,并返回增加后的值,这保证了几个进程同时进行增加操作时,增量是完全的.即不会出现n个进程对同一个 key 进行 incr(key,1) 操作后,增量小于 n 的现象.

还可以对订单进行分布式生成.具体做法是,将redis主机ID(如果是集群redis)+订单时间戳(精确到秒)+substr(md5(用户ip+user-agent))作为一个key.对属于此范围的订单进行incr操作,生成序号number,作为后缀.最后将key+number作为订单号.

或者再简单点,主机ID+时间戳+number .主机ID可以区分不同主机相同序号的订单.时间戳区分同一主机redis崩溃后恢复,内存被清除,重新计数导致的相同序号的订单(但要保证1s后再重启).如果可以恢复崩溃前的数据,则可以省掉时间戳.

这样的订单号是绝不会重复的.而且生成速度很快.这种方法和方法二类似,都是通过维护计数器.不同的是,方法二的计数器是全局的,而这种方法的计数器只对某个范围的订单计数.这样在mysql的订单号列可以不用加 unique key.

{'fraction_non_alphanumeric': 0.11817367949865712, 'fraction_numerical': 0.04237541032527604, 'mean_word_length': 11.55430711610487, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3832738', 'n_tokens_mistral': 2694, 'n_tokens_neox': 3038, 'n_words': 111}