资讯详情

资讯详情

Android App ID设计:所有ID都用12位随机数字?用不着

一个朋友前几天扔给我一张表是他们团队内部定的“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 或者雪花 IDSnowflake都行。雪花 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 位随机数字——用不着。你需要的不是一张“全项目一行规矩”的统一规范而是一张“分用途选择策略”的决策表。按需设计各司其职这才是真的简单。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →