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