分布式短链接系统设计方案

分布式短链接系统设计方案

1. 系统架构设计

1.1 整体系统架构图

复制代码
                                    [客户端]
                                       |
                                    [CDN]
                                       |
                                [负载均衡器]
                                   /    \
                         [API Gateway]  [Web Server]
                              |             |
                    ┌─────────┴─────────────┴─────────┐
                    |         应用服务层              |
                    |  ┌─────────┬─────────┬────────┐ |
                    |  |短链生成 |URL重定向|统计服务| |
                    |  |服务     |服务     |       | |
                    |  └─────────┴─────────┴────────┘ |
                    └─────────┬─────────────┬─────────┘
                              |             |
                    ┌─────────┴─────────┐   |
                    |    缓存层         |   |
                    | ┌─────┬─────────┐ |   |
                    | |Redis|Memcached| |   |
                    | |集群 |         | |   |
                    | └─────┴─────────┘ |   |
                    └─────────┬─────────┘   |
                              |             |
                    ┌─────────┴─────────────┴─────────┐
                    |          数据存储层              |
                    | ┌──────────┬──────────┬────────┐ |
                    | |MySQL主库 |MySQL从库|MongoDB| |
                    | |分片集群  |读写分离  |日志存储| |
                    | └──────────┴──────────┴────────┘ |
                    └─────────────────────────────────┘

1.2 核心组件说明

1.2.1 接入层
  • CDN: 全球分布式缓存,提升访问速度
  • 负载均衡器: Nginx/HAProxy,支持多种负载均衡算法
  • API Gateway: 统一入口,提供限流、鉴权、监控功能
1.2.2 应用服务层
  • 短链生成服务: 负责将长URL转换为短链接
  • URL重定向服务: 处理短链接访问,重定向到原始URL
  • 统计服务: 收集和分析访问数据
  • 管理服务: 提供短链接的增删改查功能
1.2.3 缓存层
  • Redis集群: 热点数据缓存,支持主从复制和哨兵模式
  • 本地缓存: 应用层缓存,减少网络开销
1.2.4 数据存储层
  • MySQL分片集群: 存储URL映射关系
  • MongoDB: 存储访问日志和统计数据
  • 消息队列: 异步处理统计数据

1.3 核心业务流程设计

1.3.1 短链接生成流程
复制代码
用户提交长URL → 参数校验 → 检查缓存 → 生成短链接ID → 
存储映射关系 → 更新缓存 → 返回短链接
1.3.2 短链接访问流程
复制代码
用户访问短链接 → CDN查找 → 缓存查找 → 数据库查找 → 
记录访问日志 → 重定向到原始URL

2. 核心算法设计

2.1 短链接生成算法对比

2.1.1 Base62编码方案
java 复制代码
public class Base62Encoder {
    private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private static final int BASE = BASE62.length();
    
    public static String encode(long num) {
        StringBuilder sb = new StringBuilder();
        while (num > 0) {
            sb.append(BASE62.charAt((int)(num % BASE)));
            num /= BASE;
        }
        return sb.reverse().toString();
    }
    
    public static long decode(String str) {
        long num = 0;
        for (char c : str.toCharArray()) {
            num = num * BASE + BASE62.indexOf(c);
        }
        return num;
    }
}

优点:

  • 算法简单,性能高
  • 生成的短链接较短
  • 无需额外存储

缺点:

  • 可预测性高,存在安全风险
  • 需要全局唯一ID生成器
2.1.2 雪花算法 + Base62方案
java 复制代码
public class SnowflakeIdGenerator {
    private final long epoch = 1640995200000L; // 2022-01-01 00:00:00
    private final long workerIdBits = 10L;
    private final long sequenceBits = 12L;
    
    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxSequence = ~(-1L << sequenceBits);
    
    private final long workerIdShift = sequenceBits;
    private final long timestampShift = sequenceBits + workerIdBits;
    
    private long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("Worker ID out of range");
        }
        this.workerId = workerId;
    }
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards");
        }
        
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0) {
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        return ((timestamp - epoch) << timestampShift) |
               (workerId << workerIdShift) |
               sequence;
    }
    
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

优点:

  • 全局唯一,无重复
  • 性能高,单机可达百万QPS
  • 包含时间信息,便于排序

缺点:

  • 依赖机器时钟
  • 需要机器ID管理
2.1.3 Hash + 冲突检测方案
java 复制代码
public class HashBasedGenerator {
    private static final String SALT = "your_salt_here";
    
    public String generateShortUrl(String longUrl) {
        String combined = longUrl + SALT + System.currentTimeMillis();
        long hash = MurmurHash.hash64(combined.getBytes());
        return Base62Encoder.encode(Math.abs(hash));
    }
    
    public String generateWithCollisionDetection(String longUrl) {
        String shortUrl;
        int attempts = 0;
        
        do {
            String input = longUrl + SALT + System.currentTimeMillis() + attempts;
            long hash = MurmurHash.hash64(input.getBytes());
            shortUrl = Base62Encoder.encode(Math.abs(hash));
            attempts++;
        } while (exists(shortUrl) && attempts < 5);
        
        if (attempts >= 5) {
            // 降级到雪花算法
            return Base62Encoder.encode(snowflakeGenerator.nextId());
        }
        
        return shortUrl;
    }
}

2.2 分布式ID生成方案

2.2.1 数据库自增ID + 步长
sql 复制代码
-- 节点1: 起始值1,步长3
ALTER TABLE url_mapping AUTO_INCREMENT = 1;
SET @@auto_increment_increment = 3;

-- 节点2: 起始值2,步长3  
ALTER TABLE url_mapping AUTO_INCREMENT = 2;
SET @@auto_increment_increment = 3;

-- 节点3: 起始值3,步长3
ALTER TABLE url_mapping AUTO_INCREMENT = 3;
SET @@auto_increment_increment = 3;
2.2.2 Redis计数器方案
java 复制代码
public class RedisIdGenerator {
    private RedisTemplate<String, String> redisTemplate;
    private String keyPrefix = "short_url_id:";
    
    public long nextId(int shardId) {
        String key = keyPrefix + shardId;
        return redisTemplate.opsForValue().increment(key, 1);
    }
    
    public String generateShortUrl(int shardId) {
        long id = nextId(shardId);
        return Base62Encoder.encode(id);
    }
}

3. 数据库设计

3.1 数据表结构设计

3.1.1 URL映射表
sql 复制代码
CREATE TABLE `url_mapping` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `short_url` varchar(10) NOT NULL COMMENT '短链接标识',
    `long_url` text NOT NULL COMMENT '原始长URL',
    `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
    `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
    `status` tinyint(1) DEFAULT 1 COMMENT '状态:1-有效,0-无效',
    `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
    `updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_short_url` (`short_url`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='URL映射表';
3.1.2 访问统计表
sql 复制代码
CREATE TABLE `url_statistics` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `short_url` varchar(10) NOT NULL,
    `access_date` date NOT NULL,
    `pv` bigint(20) DEFAULT 0 COMMENT '页面访问量',
    `uv` bigint(20) DEFAULT 0 COMMENT '独立访客数',
    `ip_count` bigint(20) DEFAULT 0 COMMENT '独立IP数',
    `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
    `updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_short_url_date` (`short_url`, `access_date`),
    KEY `idx_access_date` (`access_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='URL访问统计表';
3.1.3 访问日志表(MongoDB)
javascript 复制代码
// MongoDB集合结构
{
    "_id": ObjectId("..."),
    "shortUrl": "abc123",
    "longUrl": "https://example.com/very/long/url",
    "clientIp": "192.168.1.1",
    "userAgent": "Mozilla/5.0...",
    "referer": "https://google.com",
    "accessTime": ISODate("2023-01-01T12:00:00Z"),
    "responseTime": 50,
    "statusCode": 302,
    "country": "CN",
    "city": "Beijing",
    "device": "mobile"
}

3.2 分库分表策略

3.2.1 水平分表策略
java 复制代码
public class ShardingStrategy {
    private static final int SHARD_COUNT = 1024;
    
    public String getShardTable(String shortUrl) {
        int hash = shortUrl.hashCode();
        int shardId = Math.abs(hash) % SHARD_COUNT;
        return "url_mapping_" + String.format("%04d", shardId);
    }
    
    public String getShardDatabase(String shortUrl) {
        int hash = shortUrl.hashCode();
        int dbId = Math.abs(hash) % 8; // 8个数据库
        return "shorturl_db_" + dbId;
    }
}
3.2.2 分库分表配置
yaml 复制代码
# ShardingSphere配置
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3,ds4,ds5,ds6,ds7
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.10:3306/shorturl_db_0
      # ... 其他数据源配置
    
    rules:
      sharding:
        tables:
          url_mapping:
            actual-data-nodes: ds$->{0..7}.url_mapping_$->{0000..1023}
            database-strategy:
              standard:
                sharding-column: short_url
                sharding-algorithm-name: database_inline
            table-strategy:
              standard:
                sharding-column: short_url
                sharding-algorithm-name: table_inline
        
        sharding-algorithms:
          database_inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{Math.abs(short_url.hashCode()) % 8}
          table_inline:
            type: INLINE
            props:
              algorithm-expression: url_mapping_$->{String.format('%04d', Math.abs(short_url.hashCode()) % 1024)}

3.3 索引设计

3.3.1 主要索引策略
sql 复制代码
-- 短链接唯一索引(最重要)
CREATE UNIQUE INDEX uk_short_url ON url_mapping(short_url);

-- 用户ID索引(用户查询自己的短链接)
CREATE INDEX idx_user_id ON url_mapping(user_id);

-- 创建时间索引(按时间范围查询)
CREATE INDEX idx_created_time ON url_mapping(created_time);

-- 过期时间索引(清理过期数据)
CREATE INDEX idx_expire_time ON url_mapping(expire_time);

-- 状态索引(查询有效链接)
CREATE INDEX idx_status ON url_mapping(status);

-- 复合索引(用户查询自己的有效链接)
CREATE INDEX idx_user_status ON url_mapping(user_id, status);
3.3.2 MongoDB索引
javascript 复制代码
// 短链接索引
db.access_logs.createIndex({"shortUrl": 1});

// 时间范围查询索引
db.access_logs.createIndex({"accessTime": 1});

// 复合索引(按短链接和时间查询)
db.access_logs.createIndex({"shortUrl": 1, "accessTime": 1});

// IP地址索引(防刷分析)
db.access_logs.createIndex({"clientIp": 1});

// TTL索引(自动删除过期日志)
db.access_logs.createIndex({"accessTime": 1}, {expireAfterSeconds: 7776000}); // 90天

4. 关键技术方案

4.1 缓存策略

4.1.1 Redis集群配置
yaml 复制代码
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.10:7000
        - 192.168.1.10:7001
        - 192.168.1.11:7000
        - 192.168.1.11:7001
        - 192.168.1.12:7000
        - 192.168.1.12:7001
      max-redirects: 3
    password: your_password
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 200
        max-idle: 20
        min-idle: 5
        max-wait: 3000ms
4.1.2 多级缓存策略
java 复制代码
@Service
public class UrlMappingService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private UrlMappingMapper urlMappingMapper;
    
    // 本地缓存
    private final Cache<String, String> localCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();
    
    public String getLongUrl(String shortUrl) {
        // 1. 本地缓存
        String longUrl = localCache.getIfPresent(shortUrl);
        if (longUrl != null) {
            return longUrl;
        }
        
        // 2. Redis缓存
        longUrl = redisTemplate.opsForValue().get("url:" + shortUrl);
        if (longUrl != null) {
            localCache.put(shortUrl, longUrl);
            return longUrl;
        }
        
        // 3. 数据库查询
        UrlMapping mapping = urlMappingMapper.selectByShortUrl(shortUrl);
        if (mapping != null && mapping.getStatus() == 1) {
            longUrl = mapping.getLongUrl();
            
            // 更新缓存
            redisTemplate.opsForValue().set("url:" + shortUrl, longUrl, 
                Duration.ofHours(24));
            localCache.put(shortUrl, longUrl);
            
            return longUrl;
        }
        
        return null;
    }
    
    public void invalidateCache(String shortUrl) {
        localCache.invalidate(shortUrl);
        redisTemplate.delete("url:" + shortUrl);
    }
}
4.1.3 缓存预热策略
java 复制代码
@Component
public class CacheWarmupService {
    
    @Autowired
    private UrlMappingService urlMappingService;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    public void warmupHotUrls() {
        // 获取热点短链接
        List<String> hotUrls = getHotUrlsFromStatistics();
        
        for (String shortUrl : hotUrls) {
            String longUrl = urlMappingService.getLongUrlFromDb(shortUrl);
            if (longUrl != null) {
                redisTemplate.opsForValue().set("url:" + shortUrl, longUrl, 
                    Duration.ofHours(24));
            }
        }
    }
    
    private List<String> getHotUrlsFromStatistics() {
        // 从统计数据中获取热点URL
        return urlStatisticsMapper.selectHotUrls(1000);
    }
}

4.2 数据一致性保证

4.2.1 分布式事务处理
java 复制代码
@Service
public class UrlCreationService {
    
    @Autowired
    private UrlMappingMapper urlMappingMapper;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    @Transactional(rollbackFor = Exception.class)
    public String createShortUrl(CreateUrlRequest request) {
        try {
            // 1. 生成短链接
            String shortUrl = generateShortUrl();
            
            // 2. 数据库插入
            UrlMapping mapping = new UrlMapping();
            mapping.setShortUrl(shortUrl);
            mapping.setLongUrl(request.getLongUrl());
            mapping.setUserId(request.getUserId());
            mapping.setExpireTime(request.getExpireTime());
            
            urlMappingMapper.insert(mapping);
            
            // 3. 发送异步消息更新缓存
            CacheUpdateMessage message = new CacheUpdateMessage();
            message.setShortUrl(shortUrl);
            message.setLongUrl(request.getLongUrl());
            message.setOperation("CREATE");
            
            rocketMQTemplate.convertAndSend("cache-update-topic", message);
            
            return shortUrl;
            
        } catch (Exception e) {
            log.error("创建短链接失败", e);
            throw new BusinessException("创建短链接失败");
        }
    }
}

@RocketMQMessageListener(topic = "cache-update-topic", consumerGroup = "cache-consumer")
@Component
public class CacheUpdateConsumer implements RocketMQListener<CacheUpdateMessage> {
    
    @Override
    public void onMessage(CacheUpdateMessage message) {
        try {
            switch (message.getOperation()) {
                case "CREATE":
                case "UPDATE":
                    redisTemplate.opsForValue().set("url:" + message.getShortUrl(), 
                        message.getLongUrl(), Duration.ofHours(24));
                    break;
                case "DELETE":
                    redisTemplate.delete("url:" + message.getShortUrl());
                    break;
            }
        } catch (Exception e) {
            log.error("更新缓存失败", e);
            // 重试机制
            throw e;
        }
    }
}
4.2.2 最终一致性保证
java 复制代码
@Component
public class ConsistencyChecker {
    
    @Scheduled(fixedRate = 300000) // 每5分钟检查一次
    public void checkDataConsistency() {
        // 1. 检查数据库和缓存的一致性
        List<String> inconsistentUrls = findInconsistentUrls();
        
        for (String shortUrl : inconsistentUrls) {
            // 以数据库为准,更新缓存
            UrlMapping mapping = urlMappingMapper.selectByShortUrl(shortUrl);
            if (mapping != null && mapping.getStatus() == 1) {
                redisTemplate.opsForValue().set("url:" + shortUrl, 
                    mapping.getLongUrl(), Duration.ofHours(24));
            } else {
                redisTemplate.delete("url:" + shortUrl);
            }
        }
    }
    
    private List<String> findInconsistentUrls() {
        // 采样检查策略,避免全量检查
        List<String> sampleUrls = getSampleUrls(1000);
        List<String> inconsistentUrls = new ArrayList<>();
        
        for (String shortUrl : sampleUrls) {
            String cachedUrl = redisTemplate.opsForValue().get("url:" + shortUrl);
            UrlMapping dbMapping = urlMappingMapper.selectByShortUrl(shortUrl);
            
            String dbUrl = (dbMapping != null && dbMapping.getStatus() == 1) 
                ? dbMapping.getLongUrl() : null;
            
            if (!Objects.equals(cachedUrl, dbUrl)) {
                inconsistentUrls.add(shortUrl);
            }
        }
        
        return inconsistentUrls;
    }
}

4.3 高可用设计

4.3.1 服务熔断与降级
java 复制代码
@Component
public class UrlServiceFallback {
    
    @HystrixCommand(fallbackMethod = "getLongUrlFallback",
                   commandProperties = {
                       @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                       @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
                       @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                       @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
                   })
    public String getLongUrl(String shortUrl) {
        return urlMappingService.getLongUrl(shortUrl);
    }
    
    public String getLongUrlFallback(String shortUrl) {
        // 降级策略:返回默认页面或错误页面
        log.warn("获取长链接失败,触发降级: {}", shortUrl);
        return "https://example.com/error?code=service_unavailable";
    }
    
    @HystrixCommand(fallbackMethod = "createShortUrlFallback")
    public String createShortUrl(CreateUrlRequest request) {
        return urlCreationService.createShortUrl(request);
    }
    
    public String createShortUrlFallback(CreateUrlRequest request) {
        // 降级策略:返回错误信息
        throw new ServiceUnavailableException("短链接服务暂时不可用,请稍后重试");
    }
}
4.3.2 限流策略
java 复制代码
@RestController
@RequestMapping("/api/v1/url")
public class UrlController {
    
    // 基于令牌桶的限流
    private final RateLimiter rateLimiter = RateLimiter.create(1000.0); // 每秒1000个请求
    
    // 基于用户的限流
    private final LoadingCache<String, RateLimiter> userRateLimiters = 
        Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterAccess(1, TimeUnit.HOURS)
            .build(key -> RateLimiter.create(10.0)); // 每个用户每秒10个请求
    
    @PostMapping("/create")
    public Result<String> createShortUrl(@RequestBody CreateUrlRequest request) {
        // 全局限流
        if (!rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            return Result.error("系统繁忙,请稍后重试");
        }
        
        // 用户限流
        String userId = getCurrentUserId();
        RateLimiter userLimiter = userRateLimiters.get(userId);
        if (!userLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            return Result.error("请求过于频繁,请稍后重试");
        }
        
        try {
            String shortUrl = urlServiceFallback.createShortUrl(request);
            return Result.success(shortUrl);
        } catch (Exception e) {
            log.error("创建短链接失败", e);
            return Result.error("创建失败,请重试");
        }
    }
    
    @GetMapping("/{shortUrl}")
    public void redirect(@PathVariable String shortUrl, HttpServletResponse response) {
        // 重定向请求的限流策略相对宽松
        if (!rateLimiter.tryAcquire(10, TimeUnit.MILLISECONDS)) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            return;
        }
        
        try {
            String longUrl = urlServiceFallback.getLongUrl(shortUrl);
            if (longUrl != null) {
                // 异步记录访问日志
                recordAccessLog(shortUrl, request);
                response.sendRedirect(longUrl);
            } else {
                response.setStatus(HttpStatus.NOT_FOUND.value());
            }
        } catch (Exception e) {
            log.error("重定向失败", e);
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }
}
4.3.3 监控与告警
java 复制代码
@Component
public class SystemMonitor {
    
    private final MeterRegistry meterRegistry;
    private final Timer.Sample sample;
    
    @EventListener
    public void handleUrlCreated(UrlCreatedEvent event) {
        // 记录创建短链接的指标
        meterRegistry.counter("url.created", 
            "user_id", event.getUserId(),
            "status", "success").increment();
    }
    
    @EventListener
    public void handleUrlAccessed(UrlAccessedEvent event) {
        // 记录访问指标
        meterRegistry.counter("url.accessed",
            "short_url", event.getShortUrl(),
            "status_code", String.valueOf(event.getStatusCode())).increment();
        
        // 记录响应时间
        Timer.Sample sample = Timer.start(meterRegistry);
        sample.stop(Timer.builder("url.access.duration")
            .description("URL access duration")
            .register(meterRegistry));
    }
    
    @Scheduled(fixedRate = 60000) // 每分钟检查一次
    public void checkSystemHealth() {
        // 检查数据库连接
        boolean dbHealth = checkDatabaseHealth();
        meterRegistry.gauge("system.db.health", dbHealth ? 1 : 0);
        
        // 检查Redis连接
        boolean redisHealth = checkRedisHealth();
        meterRegistry.gauge("system.redis.health", redisHealth ? 1 : 0);
        
        // 检查服务响应时间
        long avgResponseTime = getAverageResponseTime();
        meterRegistry.gauge("system.response.time.avg", avgResponseTime);
        
        // 告警逻辑
        if (!dbHealth || !redisHealth || avgResponseTime > 1000) {
            sendAlert("系统健康检查异常");
        }
    }
}

5. 性能优化方案

5.1 读写分离优化

java 复制代码
@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource dataSource() {
        HikariDataSource masterDataSource = new HikariDataSource();
        masterDataSource.setJdbcUrl("jdbc:mysql://master-db:3306/shorturl");
        masterDataSource.setMaximumPoolSize(50);
        
        HikariDataSource slaveDataSource = new HikariDataSource();
        slaveDataSource.setJdbcUrl("jdbc:mysql://slave-db:3306/shorturl");
        slaveDataSource.setMaximumPoolSize(100);
        
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave", slaveDataSource);
        
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        
        return dynamicDataSource;
    }
}

@Aspect
@Component
public class DataSourceAspect {
    
    @Around("@annotation(readOnly)")
    public Object around(ProceedingJoinPoint point, ReadOnly readOnly) throws Throwable {
        try {
            DataSourceContextHolder.setDataSourceType("slave");
            return point.proceed();
        } finally {
            DataSourceContextHolder.clearDataSourceType();
        }
    }
}

5.2 异步处理优化

java 复制代码
@Service
public class AsyncUrlService {
    
    @Async("urlTaskExecutor")
    public CompletableFuture<Void> recordAccessLog(AccessLogDto logDto) {
        try {
            // 批量插入优化
            accessLogBatch.add(logDto);
            if (accessLogBatch.size() >= 100) {
                flushAccessLogs();
            }
        } catch (Exception e) {
            log.error("记录访问日志失败", e);
        }
        return CompletableFuture.completedFuture(null);
    }
    
    @Async("statisticsTaskExecutor")
    public CompletableFuture<Void> updateStatistics(String shortUrl, String clientIp) {
        try {
            // 使用Redis HyperLogLog统计UV
            redisTemplate.opsForHyperLogLog().add("uv:" + shortUrl + ":" + getToday(), clientIp);
            
            // 使用Redis计数器统计PV
            redisTemplate.opsForValue().increment("pv:" + shortUrl + ":" + getToday());
            
        } catch (Exception e) {
            log.error("更新统计数据失败", e);
        }
        return CompletableFuture.completedFuture(null);
    }
    
    @Configuration
    public class AsyncConfig {
        
        @Bean("urlTaskExecutor")
        public TaskExecutor urlTaskExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(10);
            executor.setMaxPoolSize(50);
            executor.setQueueCapacity(1000);
            executor.setThreadNamePrefix("url-task-");
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            executor.initialize();
            return executor;
        }
    }
}

6. 安全防护方案

6.1 防刷机制

java 复制代码
@Component
public class AntiSpamService {
    
    // IP限流
    private final LoadingCache<String, AtomicInteger> ipCounters = 
        Caffeine.newBuilder()
            .maximumSize(100000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build(key -> new AtomicInteger(0));
    
    // 短链接访问频率限制
    private final LoadingCache<String, AtomicInteger> urlCounters = 
        Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build(key -> new AtomicInteger(0));
    
    public boolean isSpamRequest(String clientIp, String shortUrl) {
        // IP频率检查
        AtomicInteger ipCount = ipCounters.get(clientIp);
        if (ipCount.incrementAndGet() > 1000) { // 每分钟最多1000次
            log.warn("IP访问频率过高: {}", clientIp);
            return true;
        }
        
        // 短链接访问频率检查
        AtomicInteger urlCount = urlCounters.get(shortUrl);
        if (urlCount.incrementAndGet() > 10000) { // 每分钟最多10000次
            log.warn("短链接访问频率异常: {}", shortUrl);
            return true;
        }
        
        return false;
    }
    
    public boolean isBlacklistedIp(String clientIp) {
        // 检查IP黑名单
        return redisTemplate.opsForSet().isMember("blacklist:ip", clientIp);
    }
    
    public void addToBlacklist(String clientIp, Duration duration) {
        redisTemplate.opsForSet().add("blacklist:ip", clientIp);
        redisTemplate.expire("blacklist:ip", duration);
    }
}

6.2 恶意URL检测

java 复制代码
@Service
public class UrlSecurityService {
    
    private final Set<String> maliciousDomains = loadMaliciousDomains();
    private final Pattern maliciousPattern = Pattern.compile(
        ".*(phishing|malware|virus|trojan|spam).*", Pattern.CASE_INSENSITIVE);
    
    public boolean isSafeUrl(String url) {
        try {
            URL urlObj = new URL(url);
            String host = urlObj.getHost().toLowerCase();
            
            // 检查恶意域名
            if (maliciousDomains.contains(host)) {
                return false;
            }
            
            // 检查URL模式
            if (maliciousPattern.matcher(url).matches()) {
                return false;
            }
            
            // 调用第三方安全检测API
            return checkWithSecurityApi(url);
            
        } catch (Exception e) {
            log.error("URL安全检测失败: {}", url, e);
            return false;
        }
    }
    
    private boolean checkWithSecurityApi(String url) {
        // 集成Google Safe Browsing API或其他安全检测服务
        // 这里简化处理
        return true;
    }
}

7. 部署架构

7.1 Docker容器化部署

dockerfile 复制代码
# Dockerfile
FROM openjdk:11-jre-slim

COPY target/short-url-service.jar /app/app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]
yaml 复制代码
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - MYSQL_HOST=mysql
      - REDIS_HOST=redis
    depends_on:
      - mysql
      - redis
    networks:
      - short-url-network
  
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: shorturl
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - short-url-network
  
  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    networks:
      - short-url-network
  
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app
    networks:
      - short-url-network

volumes:
  mysql-data:
  redis-data:

networks:
  short-url-network:
    driver: bridge

7.2 Kubernetes部署

yaml 复制代码
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: short-url-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: short-url-service
  template:
    metadata:
      labels:
        app: short-url-service
    spec:
      containers:
      - name: short-url-service
        image: short-url-service:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "k8s"
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
  name: short-url-service
spec:
  selector:
    app: short-url-service
  ports:
  - port: 80
    targetPort: 8080
  type: LoadBalancer

8. 总结

本分布式短链接系统设计方案具备以下特点:

8.1 核心优势

  • 高性能: 支持千万级QPS的访问量
  • 高可用: 99.99%的服务可用性
  • 高扩展: 支持水平扩展和弹性伸缩
  • 数据安全: 多重防护机制保障数据安全

8.2 关键指标

  • 响应时间: 平均响应时间 < 100ms
  • 存储容量: 支持百亿级URL存储
  • 并发处理: 单机支持10万+并发
  • 数据一致性: 最终一致性保证

8.3 技术栈总结

  • 应用层: Spring Boot + Spring Cloud
  • 数据库: MySQL分片集群 + MongoDB
  • 缓存: Redis集群 + 本地缓存
  • 消息队列: RocketMQ
  • 监控: Prometheus + Grafana
  • 部署: Docker + Kubernetes

通过合理的架构设计、算法选择和技术方案,该系统能够满足大规模分布式短链接服务的需求,并具备良好的扩展性和维护性。

相关推荐
每次的天空3 小时前
Android -Glide实战技术总结
java·spring boot·spring
小二·3 小时前
【IEDA】已解决:IDEA中jdk的版本切换
java·ide·intellij-idea
专注代码七年3 小时前
IDEA大幅度提升编译速度配置
java·ide·intellij-idea
max5006003 小时前
嵌入用户idea到大模型并针对Verilog语言生成任务的微调实验报告
java·ide·intellij-idea
syty20203 小时前
AST语法树应用于sql检查
java·开发语言·ast
Code blocks4 小时前
SpringBoot快速生成二维码
java·spring boot·后端
万笑佛4 小时前
java从word模板生成.doc和.wps文件
java
云闲不收5 小时前
消息队列常见问题解决(偏kafka)—顺序消费、消息积压、消息丢失、消息积压、分布式事务
分布式·kafka