基于 Java 代码实现游戏服务器生成全局唯一 ID 的方法汇总
这里有新鲜出炉的 Java 设计模式, 程序狗速度看过来!
Java 程序设计语言
java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言, 是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台 (即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se)) 的总称
我们在做服务器系统开发的时候, 为了适应数据大并发的请求, 需要插入数据库之前生成一个全局的唯一 id, 纠结全局唯一 id 怎么生成呢? 下面小编给大家分享 Java 代码实现游戏服务器生成全局唯一 ID 的方法汇总, 涉及到优劣势方面的知识点, 对此感兴趣的朋友一起看看吧
在服务器系统开发时, 为了适应数据大并发的请求, 我们往往需要对数据进行异步存储, 特别是在做分布式系统时, 这个时候就不能等待插入数据库返回了取自动 id 了, 而是需要在插入数据库之前生成一个全局的唯一 id, 使用全局的唯一 id, 在游戏服务器中, 全局唯一的 id 可以用于将来合服方便, 不会出现键冲突也可以将来在业务增长的情况下, 实现分库分表, 比如某一个用户的物品要放在同一个分片内, 而这个分片段可能是根据用户 id 的范围值来确定的, 比如用户 id 大于 1000 小于 100000 的用户在一个分片内目前常用的有以下几种:
1,Java 自带的 UUID.
UUID.randomUUID().toString(), 可以通过服务程序本地产生, ID 的生成不依赖数据库的实现
优势:
本地生成 ID, 不需要进行远程调用
全局唯一不重复
水平扩展能力非常好
劣势:
ID 有 128 bits, 占用的空间较大, 需要存成字符串类型, 索引效率极低
生成的 ID 中没有带 Timestamp, 无法保证趋势递增, 数据库分库分表时不好依赖
2, 基于 Redis 的 incr 方法
Redis 本身是单线程操作的, 而 incr 更保证了一种原子递增的操作而且支持设置递增步长
优势:
部署方便, 使用简单, 只需要调用一个 redis 的 api 即可
可以多个服务器共享一个 redis 服务, 减少共享数据的开发时间
Redis 可以群集部署, 解决单点故障的问题
劣势:
如果系统太庞大的话, n 多个服务同时向 redis 请求, 会造成性能瓶颈
3, 来自 Flicker 的解决方案
这个解决方法是基于数据库自增 id 的, 它使用一个单独的数据库专门用于生成 id 详细的大家可以网上找找, 个人觉得使用挺麻烦的, 不建议使用
4,Twitter Snowflake
snowflake 是 twitter 开源的分布式 ID 生成算法, 其核心思想是: 产生一个 long 型的 ID, 使用其中 41bit 作为毫秒数, 10bit 作为机器编号, 12bit 作为毫秒内序列号这个算法单机每秒内理论上最多可以生成 1000*(2^12)个, 也就是大约 400W 的 ID, 完全能满足业务的需求
根据 snowflake 算法的思想, 我们可以根据自己的业务场景, 产生自己的全局唯一 ID 因为 Java 中 long 类型的长度是 64bits, 所以我们设计的 ID 需要控制在 64bits
优点: 高性能, 低延迟; 独立的应用; 按时间有序
缺点: 需要独立的开发和部署
比如我们设计的 ID 包含以下信息:
| 41 bits: Timestamp | 3 bits: 区域 | 10 bits: 机器编号 | 10 bits: 序列号 |
产生唯一 ID 的 Java 代码:
- /**
- * 自定义 ID 生成器
- * ID 生成规则: ID 长达 64 bits
- *
- * | 41 bits: Timestamp (毫秒) | 3 bits: 区域(机房) | 10 bits: 机器编号 | 10 bits: 序列号 |
- */
- public class GameUUID {
- // 基准时间
- private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT
- // 区域标志位数
- private final static long regionIdBits = 3L;
- // 机器标识位数
- private final static long workerIdBits = 10L;
- // 序列号识位数
- private final static long sequenceBits = 10L;
- // 区域标志 ID 最大值
- private final static long maxRegionId = -1L ^ ( - 1L << regionIdBits);
- // 机器 ID 最大值
- private final static long maxWorkerId = -1L ^ ( - 1L << workerIdBits);
- // 序列号 ID 最大值
- private final static long sequenceMask = -1L ^ ( - 1L << sequenceBits);
- // 机器 ID 偏左移 10 位
- private final static long workerIdShift = sequenceBits;
- // 业务 ID 偏左移 20 位
- private final static long regionIdShift = sequenceBits + workerIdBits;
- // 时间毫秒左移 23 位
- private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;
- private static long lastTimestamp = -1L;
- private long sequence = 0L;
- private final long workerId;
- private final long regionId;
- public GameUUID(long workerId, long regionId) {
- // 如果超出范围就抛出异常
- if (workerId > maxWorkerId || workerId < 0) {
- throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
- }
- if (regionId > maxRegionId || regionId < 0) {
- throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");
- }
- this.workerId = workerId;
- this.regionId = regionId;
- }
- public GameUUID(long workerId) {
- // 如果超出范围就抛出异常
- if (workerId > maxWorkerId || workerId < 0) {
- throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
- }
- this.workerId = workerId;
- this.regionId = 0;
- }
- public long generate() {
- return this.nextId(false, 0);
- }
- /**
- * 实际产生代码的
- *
- * @param isPadding
- * @param busId
- * @return
- */
- private synchronized long nextId(boolean isPadding, long busId) {
- long timestamp = timeGen();
- long paddingnum = regionId;
- if (isPadding) {
- paddingnum = busId;
- }
- if (timestamp < lastTimestamp) {
- try {
- throw new Exception("Clock moved backwards. Refusing to generate id for" + (lastTimestamp - timestamp) + "milliseconds");
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- // 如果上次生成时间和当前时间相同, 在同一毫秒内
- if (lastTimestamp == timestamp) {
- //sequence 自增, 因为 sequence 只有 10bit, 所以和 sequenceMask 相与一下, 去掉高位
- sequence = (sequence + 1) & sequenceMask;
- // 判断是否溢出, 也就是每毫秒内超过 1024, 当为 1024 时, 与 sequenceMask 相与, sequence 就等于 0
- if (sequence == 0) {
- // 自旋等待到下一毫秒
- timestamp = tailNextMillis(lastTimestamp);
- }
- } else {
- // 如果和上次生成时间不同, 重置 sequence, 就是下一毫秒开始, sequence 计数重新从 0 开始累加,
- // 为了保证尾数随机性更大一些, 最后一位设置一个随机数
- sequence = new SecureRandom().nextInt(10);
- }
- lastTimestamp = timestamp;
- return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
- }
- // 防止产生的时间比之前的时间还要小(由于 NTP 回拨等问题), 保持增量的趋势.
- private long tailNextMillis(final long lastTimestamp) {
- long timestamp = this.timeGen();
- while (timestamp <= lastTimestamp) {
- timestamp = this.timeGen();
- }
- return timestamp;
- }
- // 获取当前的时间戳
- protected long timeGen() {
- return System.currentTimeMillis();
- }
- }
使用自定义的这种方法需要注意的几点:
为了保持增长的趋势, 要避免有些服务器的时间早, 有些服务器的时间晚, 需要控制好所有服务器的时间, 而且要避免 NTP 时间服务器回拨服务器的时间; 在跨毫秒时, 序列号总是归 0, 会使得序列号为 0 的 ID 比较多, 导致生成的 ID 取模后不均匀, 所以序列号不是每次都归 0, 而是归一个 0 到 9 的随机数
上面说的这几种方式我们可以根据自己的需要去选择在游戏服务器开发中, 根据自己的游戏类型选择, 比如手机游戏, 可以使用简单的 redis 方式, 简单不容易出错, 由于这种游戏单服并发新建 id 量并不太大, 完全可以满足需要而对于大型的世界游戏服务器, 它本身就是以分布式为主的, 所以可以使用 snowflake 的方式, 上面的 snowflake 代码只是一个例子, 需要自己根据自己的需求去定制, 所以有额外的开发量, 而且要注意上述所说的注意事项
以上所述是小编给大家介绍的基于 Java 代码实现游戏服务器生成全局唯一 ID 的方法汇总, 希望对大家有所帮助, 如果大家有任何疑问请给我留言, 小编会及时回复大家的在此也非常感谢大家对 PHPERZ 网站的支持!
来源: http://www.phperz.com/article/18/0207/359608.html