Redis实现分布式ID

133 阅读5分钟

自增ID存在的问题

场景:当用户进行抢购操作时,会生成订单并数据库订单表中。

订单表使用数据库自增ID存在的问题:

  1. ID规律性太明显,容易出现信息的泄露,被不怀好意的人伪造请求;
  2. 受单表数据量的限制,MySQL中表能够存储的数据有限,会出现分库分表的情况,id不能够一直自增;

当ID规律过于明显时,存在以下缺点:

  1. 安全性问题:系统容易受到恶意攻击,例如暴力破解等。攻击者可以通过分析ID规律来推断出其他用户的ID,从而进行未授权的访问或操纵。
  2. 隐私泄露风险:泄露用户的个人信息或敏感数据被曝光。攻击者可以根据规律推测出其他用户的ID,并通过这些ID获取到相应的数据,进而侵犯用户的隐私。
  3. 数据可预测性:当ID规律太明显时,使用这些规律的攻击者可以很轻易地猜测出其他实体(如订单、交易等)的ID。这可能破坏系统的数据安全性和防伪能力。
  4. 扩展性受限:系统的扩展性造成一定影响。当系统需要处理大量并发操作时,如果ID规律过于明显,可能导致多个操作同时对同一资源进行竞争,从而增加冲突和性能瓶颈。
  5. 维护困难:系统可能需要额外的资源和机制来保持规律的更新和变化,以确保安全性和数据完整性。这会增加系统的复杂度,并给维护带来挑战。

在MySQL中,表最多可以存储的记录数取决于多个因素,包括数据库版本、操作系统和硬件配置等。下面是一些常见的限制:

  1. 行数限制:在MySQL 5.7及之前的版本中,InnoDB和XtraDB存储引擎的行数限制为最大约64亿(2^{32}-1)。而在MySQL 8.0及以后的版本中,它们的行数限制可达到理论上的最大值,大约是1844万亿(2^{64}-1)行。
  2. 数据库文件大小限制:每个InnoDB表的存储大小受到所使用文件系统的限制。对于InnoDB表,默认情况下,数据库文件的大小限制取决于操作系统和文件系统,通常在几TB或更高。但是,这也可能受到特定的操作系统和文件系统的限制。
  3. 硬件资源限制:实际上,表的记录数还受到可用硬件资源,如磁盘空间、内存和处理能力的限制。当数据库文件较大时,磁盘空间变得关键,而在执行查询时,内存和处理能力可影响读写性能。

业界流传是500万行。超过500万行就要考虑分表分库了。阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,就需要考虑分库分表了

分布式ID解决自增ID的问题

分布式ID(也叫全局唯一ID),分布式ID满足以下特点:

  1. 全局唯一性:分布式ID保证在整个分布式系统中唯一性,不会出现重复的标识符。
  2. 高可用性:分布式ID生成器通常被设计为高可用的组件,可以通过水平扩展、冗余备份或集群部署来确保服务的可用性。即使某个节点或组件发生故障,仍然能够正常生成唯一的ID标识符。
  3. 安全性:分布式ID生成器通常是独立于应用程序和业务逻辑的。它们被设计为一个单独的组件或服务,可以被各种应用程序和服务所共享和使用,使得各个应用程序之间的ID生成过程互不干扰。
  4. 高性能:分布式ID生成器通常要求在很短的时间内生成唯一的标识符。为了实现低延迟,设计者通常采用高效的算法和数据结构,以及优化的网络通信和存储策略。
  5. 递增性:分布式ID通常可以被设计成可按时间顺序排序,以便更容易对生成的ID进行索引、检索或排序操作。这对于一些场景,如日志记录和事件溯源等,非常重要。

分布式ID的实现

分布式ID的实现方式:

  1. UUID
  2. Redis自增
  3. 数据库自增
  4. snowflake算法(雪花算法)

本节使用自定义的方式实现分布式ID:符号位+时间戳+序列号

  • 符号位:1bit,永远为0(表示正数);
  • 时间戳:31bit,以秒为单位,可以使用69年( 2^31/3600/24/365≈69);
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID。

为了增加ID的安全性,可以不直接使用Redis自增的数值,而是拼接一些其它信息,比如时间戳、UUID、业务关键词。

代码实现

@Component
public class RedisWorkID {

    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200;

    private static final long COUNT_BITS = 32L;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 生成分布式id
     *
     * @param keyPrefix 业务字段
     * @return
     */
    public long nextId(String keyPrefix) {

        // 生成一个时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 使用redis自增生成序列号
        // 加入时间可以增加ID数量  也方便统计
        String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        // 此处不会发生空指针异常,increment一定会有一个结果
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        //进行组合
        return timestamp << COUNT_BITS | count;

    }

    public static void main(String[] args) {
        LocalDateTime dateTime = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        // 将时间转换成秒
        long epochSecond = dateTime.toEpochSecond(ZoneOffset.UTC);
        System.out.println("epochSecond = " + epochSecond);
    }
}