一个朋友前几天扔给我一张表,是他们团队内部定的“ID规范”,大意是:App 里头所有跟业务相关的 ID,一律生成 12 位随机数字,统一走同一个接口,谁都不能例外,理由是“简单、统一、安全”。我盯着那张表看了半天,回了一句:这个方案,用不着。不是说随机数字不好,而是“所有 ID 全用随机数字”这个想法,从上到下都透着一股“我在用平均数描述全人类”的味道。这标题不是我起的,但我觉得起得挺准——“android app 内所有的 id 全都采用 12 位随机数字,用不着”。如果你正好在某个群聊里看过类似的讨论,或者自己也动过这个念头,那我今天这篇东西值得你花几分钟看完。
1. “全 ID 随机化”这个想法是怎么来的
先说清楚,这个念头不是凭空冒出来的。很多团队,尤其是从单机应用转过来做移动端的新团队,很容易在第一次做用户体系、订单体系的时候产生一种焦虑:怕 ID 被人遍历,怕自增 ID 暴露业务量,怕别人通过订单号猜出今天的成交量。这种焦虑一上来,第一反应就是——那我干脆把所有 ID 都变成随机数好了。12 位数字,看起来不长不短,转 long 刚刚好,不用碰字符串,也躲开了 UUID 那种“长得像乱码还带横杠”的东西。听起来一切都很美好。
而且 12 位随机数确实有一种“看着就正规”的错觉。数字短、可读性好、日志里一眼扫过去不会像 UUID 一样让人头皮发麻。给客服工单系统用,给用户分享码用,给优惠券编号用,观感都不错。但问题是,很多人把一个“局部工具”直接升级成了“全局架构规范”,好像全公司上下所有模块的 ID 都统一成一个随机数字格式,就天下太平了。这种“一刀切”的设计思路,平时看着没什么,真正被业务打脸的时候,改造成本高到你怀疑人生。
我见过最典型的案例是这样的:新来的后端同学把所有表主键都改成了随机 bigint,移动端配合着生成。结果上线第三天,运营那边要拉“今日新增用户”的数据,发现没法按时间排序了,因为随机数跟创建时间没有任何关系。你这边的理想是“所有 ID 统一生成”,那边运营同学的 Excel 里已经乱成一锅粥。这还只是开始。等到要排查线上问题,测试同学报了个 bug,说是在某个页面操作失败,开发需要根据 ID 去查日志。好家伙,那 12 位数字在日志里出现无数个,到底哪个是订单号、哪个是用户 ID、哪个是设备 ID,不靠上下文根本分不清。
“全 ID 随机化”这个想法的原始动机,我总结一下,无非四个词:防猜测、防遍历、统一格式、看着安全。这四个词单独拎出来,每一条都算合理诉求。但它们的适用场景是“敏感的唯一标识”,而不是“app 内所有 id”。把手段当目的,把局部方案当全局规范,这是这个方案从一开始就走偏的根本原因。后面我会一个个拆开讲。
2. 从“能用”到“好用”,随机 ID 在真实链路里到底卡在哪
2.1 排查问题时的“猜谜游戏”
任何方案最终都要过“线上排查”这一关。随机 ID 表面上只是让标识变得更难猜,但难猜的代价是:等你真的需要根据 ID 去定位问题的时候,它也把自己藏起来了。
举个实际例子。用户反馈说“支付页面点了没反应”,你让后台帮你看一下这条支付的记录。如果订单号是时间戳加序号生成出来的,比如 20250117153012001,扫一眼就知道是 2025 年 1 月 17 日 15 点 30 分 12 秒附近的第 1 笔订单。对应的日志一查一个准,前后 5 分钟之内的记录全都能捞出来。但如果你的订单号是一个 12 位随机数,比如 837204195620,你能从这串数字里读出什么?什么也读不出来。你只能拿到这个 ID 之后,去数据库里“精确命中”它。可万一这条支付记录因为网络异常压根没写进库里呢?随机数 ID 这时候不但帮不上忙,反而让你失去了一条最直接的检索线索。
可能有人说:那我在数据库里加一个 created_at 字段不就行了吗?行,没问题。但你让运营同学去查昨天凌晨 3 点到 4 点之间的所有异常订单,人家不会写 SQL,只会打开订单列表,按创建时间排序,然后肉眼筛选。自增 ID、时间戳 ID 天然有序,排序本身就是数据库的常规能力,不用额外做索引设计。而随机 ID,你为了“能排序”,不得不再引入一个创建时间字段,再给它建索引。这一整套做下来,增加的复杂度都是真金白银。
我做 Android 开发这些年,最烦的日志场景之一,就是客户端拿到的 ID 是随机数,后端日志里的 ID 也是随机数,两边对不上。你说 Debug 也好,说联调也好,总得有个“可读”的锚点。随机数 ID 最大的问题不是它不安全,而是它把“可读性”这个隐藏需求直接抹掉了。
2.2 数据库存储与索引的隐性成本
后端同事常说随机主键的“页分裂”问题,这里我不展开数据库原理,就说一个移动端开发能感知到的现象:当你把一个 12 位随机数当主键去数据库里按条件查询,比如按 user_id 查订单列表,每次都是随机 I/O,索引树的局部性很差。表一大了之后,同样的查询,自增 ID 可能几十毫秒,随机 ID 可能就是几百毫秒。移动端对这个不见得有直接体感,但接口响应变慢的第一责任人,往往就是这个“看起来无所谓”的随机主键。
Android 端的 SQLite 也一样。你本地缓存一张表,如果主键是自增整数,插入新数据时直接顺序追加,性能和监控都特别舒服。但如果你在本地也生成了 12 位随机数字当主键,往一个已经有一万条数据的表里插数据,页分裂和索引重建的代价就上来了。虽然本地数据量通常不至于让你卡顿到肉眼可见,但你要是在一个列表里边滑动边插入记录,用 Systrace 一抓,那些“额外耗时的 Binder 调用”可能都跟索引维护有关。
我不否认,现代数据库处理这点随机主键的压力,十有八九都扛得住。但你要知道,移动端 App 不是只跑在你一台开发机上。中低端 Android 设备的 SQLite 性能差距,比你在 Pixel 上测试的结果要离谱得多。你为了“防遍历”付出的性能代价,最终全由用户买单。
2.3 “安全”不等于“不可见”
再来说那个最有迷惑性的点:“用随机 ID 更安全”。这句话基本属于“听起来对,但经不起细想”。常见的说法是:我不希望别人能通过 user/1 这种接口猜到下一个用户 ID,然后爬我的数据。这个担心有一定道理,但它的解法不是“把 ID 随机化”,而是“接口做鉴权”。你不是防别人猜 ID,你是防别人越权访问。
实际的越权漏洞,靠的全是后端校验。你给接口加个注解,判断当前登录用户是否有权限访问这个 user ID 的数据,比把 ID 从自增改成随机数管用一万倍。反过来,如果后端校验就是有漏洞,那随机 ID 只是把“被遍历”变成了“慢一点被遍历”,根本拦不住有心人。
Android 开发里还有一层原因,App 是跑在用户设备上的,你代码里生成随机 ID 的逻辑、加密逻辑、传参逻辑,全都暴露在客户端。真要逆向你 App 的人,反编译一下,看看你有没有做加固、做混淆,那一套随机算法早晚能手撕出来。你以为你在用随机数保护业务数据,实际上你只是给真正做安全的人添了乱。安全是体系问题,不是 ID 格式问题。
3. 按用途拆 ID:时间戳、自增、随机数各有各的活法
说完了“不要一刀切”,那正确的方法是什么?很简单,按用途拆。App 里面的 ID 至少可以分成四类,每类的诉求完全不一样。
3.1 本地数据库主键:自增 / UUID 本地生成都行
先看最本分的场景——本地数据库主键,比如 Android 端的 Room 或 SQLite。这种 ID 只服务于一个目标:在本机唯一标识一条记录。它不出 App,不上服务端,也不跟其他用户发生任何交互。对这类 ID,你不需要随机化,也不需要时间戳。直接用autoincrement自增主键,清爽又高效。
@PrimaryKey(autoGenerate = true) val id: Long = 0写完,剩下的全让数据库自己管。插入顺序就是主键顺序,分页查询天然按时间倒序,还不存在碰撞问题。唯一需要注意的是:如果你有“两张本地表需要同步”,或者“本地记录跟服务端记录需要做关联”这种场景,那单靠本地自增 ID 是行不通的,你需要在表里额外存一个业务唯一标识(比如服务端返回的 ID)。那个标识就不归 Room 管了,归服务端管。
也有人说,我在本地也用 UUID 或者随机数主键,为了以后数据上云方便。这个想法不坏,但代价是本来可以白拿的自增性能没了。建议冷静评估一下:你真有“本地数据以后要同步上云”的需求吗?还是只是“万一以后要用”的边界假设?如果只是假设,那就别设计过度,自增主键足够。
3.2 服务端主键与业务单号:时序优先,别乱随机
服务端侧的主键和业务单号,是“全随机化”呼声最响亮,但最不应该随意随机化的地方。原因前面已经说过了:维护成本。
服务端主键,自增 long 或者雪花 ID(Snowflake)都行。雪花 ID 本质上就是一个 64 位 long,它的核心优势不是“随机”,而是“全局唯一 + 趋势递增”。因为它内部各段分别放了时间戳、机器标识和序列号,所以生成出来的数字看起来无序,但顺着时间趋势在涨。这既满足了你“不想被别人猜出今天有多少单”的诉求,也保留了“能按时间排序”“对索引友好”等优点,算是一个折中的“长得很像随机数,实际却有序”的方案。
业务单号,比如订单号、支付流水号这种对外展示的 ID,建议直接用“时间戳 + 业务标识 + 随机后缀”或者“时间戳 + 自增序号”的组合,而不是单独一长串随机数。我给你一个最常见的组合公式:
订单号 = yyyyMMddHHmmss + 3位业务线标识 + 4位随机数
这种设计的好处是:一看就知道是哪天创建的、属于哪个业务线,搜索日志极其方便,同时尾部还有一点随机性,不至于完全可预测。那 4 位随机数不是用来“防猜测”的,是用来“防同一秒重复”的,懂这个区别吗?随机数在这里只是修正项,不是主键本身。
3.3 设备 ID、安装 ID、推送 Token:这才是“随机数”的主场
那随机数真的没有用武之地吗?有。App 识别用户设备时最常用的device_id(每次安装重新生成)、统计 SDK 里的session_id(每次启动重新生成)、推送系统里的registration_id(每次注册重新生成),这些“生命周期短、不需要人类可读、仅仅需要唯一性”的标识,才是随机数的真正主场。
这类 ID 的最佳实践是:用 UUID(或去掉横杠的 UUID)一次性生成,存在本地SharedPreferences或DataStore里,之后每次启动都读取同一个值。它不需要有序,不需要可读,不需要跟业务强相关。你不能说“我看看这个 UUID 就能知道用户是哪天安装的”,但这条路本身就不需要你读出什么东西来。
有些 App 会把这类 ID 上传统计平台做去重和用户画像。如果统计平台的并发量高了,直接用 UUID 字符串做关联也不是不行,无非是索引大一点。再激进一点的团队会用RandomStringUtils生成更短的随机串,但那就得自己评估碰撞概率了。
3.4 分享码、邀请码等短码:随机是目的,不是手段
最后一类是邀请码、分享码、优惠券兑换码。这类 ID 有一个天然诉求——短。它们要在用户间传播、口头念、写在海报上、输进兑换框里。你不能拿一个 36 位 UUID 当邀请码,也不能拿一个 13 位时间戳当兑换码,因为用户会抄错。所以这类 ID 的设计目标其实是“高密度信息下尽量短的随机文本”。
带随机性质是必须的,否则用户 A 很容易推断出用户 B 的邀请码。但这里的随机不是“全部随机”,也需要做“可校验”。比如常见的做法是用不混淆字符集(去掉 0/O、1/I/l),加上校验位,或者在发放时查重。这些都是专门的“短码设计”学问,跟“App 内所有 ID 随机化”完全不是一个层次的方案。你要是说“优惠券码用 12 位随机数字”,我勉强能理解;但你要是说“用户 ID 也用 12 位随机数字”,我只能说你把这两个需求混为一谈了。
4. 12 位随机数的技术账:从 Random 到 SecureRandom,再到碰撞率到底多高
4.1 你用哪个 Random?
假设你确定要用 12 位随机数字了,下一个绕不开的问题就是:你用什么生成器?
如果你在 Java/Kotlin 里直接写java.util.Random,我得提醒你,这东西生成的是伪随机数,种子如果固定(比如固定传了一个值或者使用默认种子),生成的序列是可以预测的。在安全敏感场景下,用Random生成 ID 跟没随机也差不了多少。更何况 Android 上还有个历史遗留问题:在低版本系统上,多个进程同时 new Random() 可能出现序列重复。
安全一点的方案是用java.security.SecureRandom。它内部会从系统的熵源(真随机源)取种子,生成的随机序列就不容易被预测了。听起来是不是很完美?但 SecureRandom 在生成时可能阻塞,尤其在 Android 真机上首次初始化时,偶尔会出现几秒甚至更久的停顿。如果你在启动阶段调它去生成设备 ID,不做异步处理,那用户就会看到冷启动白屏。这个坑,项目刚起步的时候没人会在意,等用户量上来之后就成了卡顿热点。
第三方方案也不少,比如使用 Kotlin 的kotlin.random.Random(内部默认是伪随机,也支持自定义种子),或者用 UUID 剪裁。但不管选哪个,你都绕不开一个核心宿命:序列要么快但可预测,要么慢但不可预测。全项目所有 ID 都用同一套生成策略,就等于把“快”和“安全”的取舍问题,强行绑定了每一次生成。
我在实际项目里见过的大多数“全 ID 随机化”方案,最后代码里都躺着一个巨简单的Random.nextInt(1000000000, 9999999999)。嗯,这代码不慢,也不怎么安全。
4.2 12 位数字的空间到底有多大?碰撞率算给你看
12 位数字,范围从 1000000000 到 9999999999,也就是 90 亿个可用空间。听起来很多,但你得知道,空间大小不等于“永远不会碰撞”。
如果这 90 亿个号码里已经用了 1000 万个(一千万用户/一千万订单),那么下一次生成撞车的概率大约是 1000 万除以 90 亿,约等于 11%。单看概率,好像不是很高。但问题是,你的业务是持续增长的。如果用户量到 1 亿,新生成的 ID 碰撞概率就变成了 1 亿除以 90 亿,也就是超过 10%。更麻烦的是,ID 碰撞不像抽奖,一旦发生,它产生的后果往往不是“这次请求失败”,而是“两个用户的数据互相串了”“A 用户的记录被 B 覆盖了”。修复成本远高于让你换个自增 ID 的成本。
有人会用“先查重再插入”来规避。没问题,但这相当于每次插入都额外做一次查询,性能折损。而且在高并发场景下,“查重—插入”不是原子操作,你查完没撞,别人插了,你再插就撞了。你还得加唯一索引加锁处理。这一套下来,已经不叫“简单方案”了。
4.3 每次都从客户端生成?那多端多进程的碰撞风险更高
还有比“要不要用随机数”更要命的问题:ID 在哪儿生成?如果是在客户端生成 12 位随机 ID,那恭喜你,全世界的 Android 手机都是你的“分布式随机数发生器”。用户 A 的手机和用户 B 的手机之间互相不可见,它们各自生成了重复的 ID,你完全不知道,等到数据上报到服务端撞车才会暴雷。
大厂常用的做法是:客户端只生成“临时会话 ID” / “本地请求 ID”,一旦数据要入库,服务端会重新签发全局唯一 ID。客户端生成的 ID 权限很小,只用于关联日志和本地状态,不去污染全局主键。
所以,无论从空间大小、碰撞概率,还是从生成端的控制能力来看,全项目统一 12 位随机数字这个方案,技术账怎么算都不划算。
5. 如果还是想用随机 ID,哪些场景能接受,怎么补救
5.1 能接受的场景:低频、非持久、可容错
说完了“用不着”之后,我再说说“什么情况下用没关系”。不是所有随机 ID 都需要一棍子打死。如果场景满足以下三个条件,那用随机 ID 确实可以:
- 低频:一天生成量很少,比如管理员手动创建活动编号。
- 非持久:这个 ID 的生命周期很短,用一次就销毁,比如一次网络请求的
request_id、一次埋点的event_id。 - 可容错:就算碰撞了,影响范围很小,比如本地日志文件的主键,冲突了大不了覆盖一条日志。
符合这三条的,你爱用不用。符合三条里的任意两条,也可以谨慎考虑。但如果三个条件一个都不满足,还硬要上随机 ID,那后面一定有一堆坑等着你。
5.2 救不了的场景:主键、关联键、对账流水
反之,以下三种场景不要碰随机 ID:
- 数据库主键:随意随机替代自增/雪花,破坏索引局部性和可读性。
- 跨模块关联键:比如用户 ID 在多个 App 模块中流转、作为 join 条件、作为缓存 key,随机化之后会让所有下游排查都变难。
- 对账流水号:跟钱有关的订单号、退款号、支付流水号,必须可读、有序、可追踪。
你要是跟我说“我就用随机 ID 做订单号,反正查不到就去库里搜”,那我只能说,等到你一天要处理几万条订单对账、凌晨三点被运营电话吵醒的时候,你就懂了。
5.3 如果非要补救,有哪些低成本的折中方案
如果你的项目已经上了“12 位随机数字”这条贼船,还没法立刻下船,我给几个低成本补救建议:
把所有 ID 的生成逻辑收敛到一个类里,最好做成一个工厂,不要散落在各个业务代码里。以后想改策略,只需改这一个地方。
数据库表加
created_at字段,别偷懒,默认值设为当前时间。这样至少检索还能有一条路。在日志和上报数据里,把 “ID 类型”带出来。比如事件属性里加一个
id_type: user | order | device,让自己和后台排查的人不至于看到一串数字全懵。如果还来得及,用“自定义前缀 + 随机数”或者“时间戳前几位 + 随机数”的组合,至少保留一部分可读性。
服务端在写库之前,做一个 ID 唯一性兜底。如果发现主键冲突,丢一个重试信号,让客户端重新生成一次。这是兜底方案,治标不治本,但总比数据串了好。
6. ID 不只是“唯一”,它更是一种“维护协议”
做开发时间长了你会发现,ID 这个东西,表面上是个技术设计,实际上是一个跨角色沟通的约定。运营同学靠它对数据,客服同学靠它帮用户查单,后端同学靠它排查异常日志,甚至你自己,三个月后回来看代码,也得靠它快速定位问题。如果你把“唯一性”当成唯一指标,把“随机性”当成安全感的代名词,那最终坑的一定是未来那个在夜半三更排查问题的自己。
我在好几个项目里都用过一套非常朴素的标准来审视 ID 设计,今天拿出来分享给大家,算是我这些年踩坑踩出来的经验:
- 给机器看的 ID(数据库主键、缓存 key)优先保证性能与有序,能自增就自增,能雪花就雪花。
- 给人看的 ID(订单号、邀请码、客服工单号)优先保证可读、可分类、可检索,哪怕牺牲点长度也行。
- 给系统看的 ID(设备号、会话号、推送 token)才考虑随机,因为这时候“不可猜测”和“不可伪造”才是核心诉求。
很多团队之所以想搞“全 ID 随机化”,深层的心理其实是:害怕自己的系统被攻击、被爬虫、被刷单。但这些风险没有一个是靠随机 ID 能从根本上解决的。防爬要限流,防刷要风控,防越权要鉴权,防追踪要合规。ID 格式只是表象,别捡了芝麻丢了西瓜。
所以我的结论依然很朴素:Android App 内所有 ID 全用 12 位随机数字——用不着。你需要的不是一张“全项目一行规矩”的统一规范,而是一张“分用途选择策略”的决策表。按需设计,各司其职,这才是真的简单。