跨域(CORS)和缓存中间件(Redis)深度解析
第一部分:跨域(CORS)完全指南
一、跨域基础概念
1.1 什么是跨域?
同源策略是浏览器的一个安全机制,它限制了一个源的文档或脚本如何与另一个源的资源进行交互。
同源的定义:协议 + 域名 + 端口 完全相同
javascript
// 假设当前页面URL:http://localhost:8080/index.html
http://localhost:8080/api/users ✅ 同源
https://localhost:8080/api/users ❌ 跨域(协议不同)
http://localhost:3000/api/users ❌ 跨域(端口不同)
http://127.0.0.1:8080/api/users ❌ 跨域(域名不同)
http://localhost:8080/api/users ✅ 同源
1.2 为什么会有跨域问题?
前后端分离架构中的典型场景:
前端Vue项目:http://localhost:8081
后端Spring Boot:http://localhost:8080
浏览器:前端想访问后端API?不行!跨域了!🚫
1.3 跨域错误示例
javascript
// 浏览器控制台会显示:
Access to XMLHttpRequest at 'http://localhost:8080/api/users'
from origin 'http://localhost:8081' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
二、CORS解决方案详解
2.1 CORS工作原理
简单请求流程:
浏览器 → 发送请求(自动加Origin头) → 服务器
浏览器 ← 响应(含Access-Control-*头) ← 服务器
预检请求流程(复杂请求):
1. 浏览器 → OPTIONS预检请求 → 服务器
2. 浏览器 ← 预检响应(CORS头) ← 服务器
3. 浏览器 → 实际请求 → 服务器
4. 浏览器 ← 实际响应 ← 服务器
2.2 代码配置详解
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 1️⃣ 路径映射
.allowedOriginPatterns( // 2️⃣ 允许的源
"http://localhost:*",
"http://127.0.0.1:*",
"http://192.168.*:*",
"http://10.*:*"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 3️⃣ HTTP方法
.allowedHeaders("*") // 4️⃣ 请求头
.allowCredentials(true) // 5️⃣ 认证信息
.maxAge(3600); // 6️⃣ 预检缓存
}
}
配置项详解:
-
addMapping("/")**
/**
:对所有API路径生效/api/**
:只对/api开头的路径生效
-
allowedOriginPatterns
- 支持通配符模式(Spring 5.3+)
- 比
allowedOrigins
更灵活
-
allowedMethods
- 简单方法:GET、POST、HEAD
- 需预检的方法:PUT、DELETE、PATCH
-
allowCredentials(true)
- 允许发送Cookie
- 注意:不能与
allowedOrigins("*")
同时使用
-
maxAge(3600)
- 预检请求缓存1小时
- 减少OPTIONS请求次数
三、CORS进阶配置
3.1 细粒度控制
java
@RestController
@RequestMapping("/api")
public class UserController {
// 方法级别的跨域配置
@CrossOrigin(origins = "http://specific-domain.com")
@GetMapping("/sensitive-data")
public Result getSensitiveData() {
// ...
}
// 类级别的跨域配置
@CrossOrigin(
origins = {"http://app1.com", "http://app2.com"},
methods = {RequestMethod.GET, RequestMethod.POST},
maxAge = 3600,
allowedHeaders = {"X-Custom-Header"},
exposedHeaders = {"X-Response-Header"}
)
@RestController
public class SpecialController {
// ...
}
}
3.2 动态CORS配置
java
@Configuration
public class DynamicCorsConfig implements WebMvcConfigurer {
@Value("${app.cors.allowed-origins}")
private String[] allowedOrigins; // 从配置文件读取
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("*")
.allowCredentials(true);
}
}
3.3 安全最佳实践
java
@Configuration
public class SecureCorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 🔒 生产环境:精确配置允许的源
configuration.setAllowedOrigins(Arrays.asList(
"https://app.production.com",
"https://www.production.com"
));
// 🔒 限制允许的方法
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
// 🔒 限制允许的请求头
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"X-Requested-With"
));
// 🔒 暴露必要的响应头
configuration.setExposedHeaders(Arrays.asList(
"X-Total-Count",
"X-Page-Number"
));
// 🔒 根据需要设置认证
configuration.setAllowCredentials(true);
// 🔒 合理设置预检缓存时间
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
第二部分:Redis缓存中间件完全指南
一、Redis基础概念
1.1 什么是Redis?
Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统:
- 内存数据库:数据存储在内存中,速度极快
- 持久化支持:可以将数据保存到磁盘
- 数据结构丰富:支持String、List、Set、Hash、ZSet等
1.2 为什么需要缓存?
没有缓存的场景:
用户请求 → Controller → Service → Mapper → 数据库 → 返回
每次都查询数据库,压力大,响应慢!
有缓存的场景:
用户请求 → Controller → Service → Redis缓存(命中) → 返回
↓(未命中)
Mapper → 数据库 → 存入Redis → 返回
二、Redis配置详解
2.1 序列化配置分析
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 🔑 关键配置:序列化器
// 1. GenericJackson2JsonRedisSerializer
// 优点:自动处理类型信息,支持多态
// 缺点:存储空间稍大(包含类型信息)
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
// 2. StringRedisSerializer
// 用于key序列化,便于Redis客户端查看
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// 配置序列化器
template.setKeySerializer(stringSerializer); // key序列化
template.setHashKeySerializer(stringSerializer); // hash key序列化
template.setValueSerializer(jsonSerializer); // value序列化
template.setHashValueSerializer(jsonSerializer); // hash value序列化
template.afterPropertiesSet();
return template;
}
}
2.2 序列化方式对比
java
// 1. JDK序列化(默认,不推荐)
User user = new User();
// Redis存储:\xAC\xED\x00\x05sr\x00... (二进制,不可读)
// 2. String序列化(只能存字符串)
template.opsForValue().set("name", "张三");
// Redis存储:张三
// 3. JSON序列化(推荐)
User user = new User();
user.setUsername("admin");
// Redis存储:{"@class":"com.example.User","username":"admin"}
// 4. 自定义Jackson配置
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
三、Redis缓存实战应用
3.1 基础缓存操作
java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1️⃣ 缓存读取模式
public User getUserById(Long id) {
String key = "user:" + id;
// 查询缓存
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
log.info("缓存命中: {}", key);
return user;
}
// 查询数据库
user = userMapper.selectById(id);
if (user != null) {
// 写入缓存,设置过期时间
redisTemplate.opsForValue().set(key, user, 2, TimeUnit.HOURS);
log.info("写入缓存: {}", key);
}
return user;
}
// 2️⃣ 缓存更新模式
public User updateUser(User user) {
// 更新数据库
userMapper.update(user);
// 删除缓存(Cache Aside模式)
String key = "user:" + user.getId();
redisTemplate.delete(key);
return user;
}
// 3️⃣ 缓存删除
public boolean deleteUser(Long id) {
// 删除数据库
int result = userMapper.deleteById(id);
// 删除缓存
String key = "user:" + id;
redisTemplate.delete(key);
return result > 0;
}
}
3.2 高级缓存策略
java
@Component
public class AdvancedCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1️⃣ 防止缓存穿透
public User getUserWithNullCache(Long id) {
String key = "user:" + id;
// 检查缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
// 如果是空值标记,返回null
if ("NULL".equals(cached)) {
return null;
}
return (User) cached;
}
// 查询数据库
User user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 2, TimeUnit.HOURS);
} else {
// 缓存空值,防止缓存穿透
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
}
return user;
}
// 2️⃣ 缓存预热
@PostConstruct
public void preloadCache() {
// 启动时预加载热点数据
List<User> hotUsers = userMapper.selectHotUsers();
for (User user : hotUsers) {
String key = "user:" + user.getId();
redisTemplate.opsForValue().set(key, user, 4, TimeUnit.HOURS);
}
log.info("缓存预热完成,加载{}条数据", hotUsers.size());
}
// 3️⃣ 批量操作优化
public List<User> getUsersByIds(List<Long> ids) {
// 构建key列表
List<String> keys = ids.stream()
.map(id -> "user:" + id)
.collect(Collectors.toList());
// 批量获取
List<Object> cached = redisTemplate.opsForValue().multiGet(keys);
List<User> result = new ArrayList<>();
List<Long> missedIds = new ArrayList<>();
for (int i = 0; i < cached.size(); i++) {
if (cached.get(i) != null) {
result.add((User) cached.get(i));
} else {
missedIds.add(ids.get(i));
}
}
// 查询未命中的数据
if (!missedIds.isEmpty()) {
List<User> missedUsers = userMapper.selectByIds(missedIds);
result.addAll(missedUsers);
// 批量写入缓存
Map<String, Object> toCache = new HashMap<>();
for (User user : missedUsers) {
toCache.put("user:" + user.getId(), user);
}
redisTemplate.opsForValue().multiSet(toCache);
}
return result;
}
}
3.3 分布式锁实现
java
@Component
public class RedisLockService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 获取分布式锁
public boolean tryLock(String lockKey, String clientId, long expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
// 释放锁(使用Lua脚本保证原子性)
public boolean releaseLock(String lockKey, String clientId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(lockKey),
clientId
);
return Long.valueOf(1).equals(result);
}
// 使用示例
public void doBusinessWithLock() {
String lockKey = "lock:order:create";
String clientId = UUID.randomUUID().toString();
try {
// 尝试获取锁,最多等待5秒
boolean locked = tryLock(lockKey, clientId, 5);
if (!locked) {
throw new RuntimeException("获取锁失败");
}
// 执行业务逻辑
// ...
} finally {
// 释放锁
releaseLock(lockKey, clientId);
}
}
}
四、Redis进阶特性
4.1 数据结构应用
java
@Service
public class RedisDataStructureService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1️⃣ String类型:计数器
public Long incrementPageView(String pageId) {
String key = "pv:" + pageId;
return redisTemplate.opsForValue().increment(key);
}
// 2️⃣ Hash类型:对象存储
public void saveUserInfo(Long userId, Map<String, Object> userInfo) {
String key = "user:info:" + userId;
redisTemplate.opsForHash().putAll(key, userInfo);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
// 3️⃣ List类型:消息队列
public void pushMessage(String queueName, Object message) {
redisTemplate.opsForList().leftPush("queue:" + queueName, message);
}
public Object popMessage(String queueName) {
return redisTemplate.opsForList().rightPop("queue:" + queueName);
}
// 4️⃣ Set类型:共同好友
public Set<Object> getCommonFriends(Long userId1, Long userId2) {
String key1 = "friends:" + userId1;
String key2 = "friends:" + userId2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
// 5️⃣ ZSet类型:排行榜
public void updateScore(String rankingName, String userId, double score) {
String key = "ranking:" + rankingName;
redisTemplate.opsForZSet().add(key, userId, score);
}
public Set<ZSetOperations.TypedTuple<Object>> getTopN(String rankingName, int n) {
String key = "ranking:" + rankingName;
return redisTemplate.opsForZSet()
.reverseRangeWithScores(key, 0, n - 1);
}
}
4.2 缓存优化策略
java
@Configuration
@EnableCaching
public class CacheOptimizationConfig {
// 1️⃣ 使用Spring Cache注解
@Service
public class AnnotationCacheService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
userMapper.update(user);
return user;
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void clearAllUserCache() {
// 清除所有用户缓存
}
}
// 2️⃣ 自定义缓存管理器
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 默认过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 针对不同缓存设置不同的过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("users", config.entryTtl(Duration.ofHours(2)));
configMap.put("products", config.entryTtl(Duration.ofMinutes(30)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configMap)
.build();
}
}
五、性能监控与调优
5.1 Redis监控
java
@Component
public class RedisMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 监控Redis性能
public Map<String, Object> getRedisStats() {
return redisTemplate.execute((RedisCallback<Map<String, Object>>) connection -> {
Properties info = connection.info();
Map<String, Object> stats = new HashMap<>();
// 内存使用
stats.put("used_memory", info.getProperty("used_memory_human"));
stats.put("used_memory_peak", info.getProperty("used_memory_peak_human"));
// 连接数
stats.put("connected_clients", info.getProperty("connected_clients"));
// 命令统计
stats.put("total_commands_processed",
info.getProperty("total_commands_processed"));
// 命中率
String hits = info.getProperty("keyspace_hits");
String misses = info.getProperty("keyspace_misses");
if (hits != null && misses != null) {
long h = Long.parseLong(hits);
long m = Long.parseLong(misses);
double hitRate = h / (double)(h + m) * 100;
stats.put("hit_rate", String.format("%.2f%%", hitRate));
}
return stats;
});
}
// 慢查询日志
@Scheduled(fixedDelay = 60000)
public void checkSlowLogs() {
List<Object> slowLogs = redisTemplate.execute(
(RedisCallback<List<Object>>) connection -> {
return connection.slowLogGet(10);
}
);
if (!slowLogs.isEmpty()) {
log.warn("发现Redis慢查询: {}", slowLogs);
}
}
}
5.2 缓存问题解决方案
java
@Component
public class CacheProblemSolver {
// 1️⃣ 缓存雪崩解决方案
public void preventCacheAvalanche() {
// 设置不同的过期时间
Random random = new Random();
int baseExpire = 3600; // 基础1小时
int randomExpire = random.nextInt(600); // 随机0-10分钟
redisTemplate.expire("key", baseExpire + randomExpire, TimeUnit.SECONDS);
}
// 2️⃣ 缓存击穿解决方案
public User getUserWithMutex(Long id) {
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
// 使用互斥锁
String lockKey = "lock:user:" + id;
String clientId = UUID.randomUUID().toString();
try {
boolean locked = tryLock(lockKey, clientId, 10);
if (locked) {
// 再次检查缓存(双重检查)
user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
// 查询数据库
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 2, TimeUnit.HOURS);
}
}
} else {
// 等待一段时间后重试
Thread.sleep(100);
return getUserWithMutex(id);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
releaseLock(lockKey, clientId);
}
}
return user;
}
// 3️⃣ 热点数据处理
@Component
public class HotDataCache {
private final Map<String, Object> localCache = new ConcurrentHashMap<>();
public Object getHotData(String key) {
// 一级缓存:本地缓存
Object value = localCache.get(key);
if (value != null) {
return value;
}
// 二级缓存:Redis
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
// 设置本地缓存过期
scheduleLocalCacheExpire(key, 60);
return value;
}
// 三级:数据库
// ...
return value;
}
private void scheduleLocalCacheExpire(String key, int seconds) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.schedule(() -> localCache.remove(key), seconds, TimeUnit.SECONDS);
}
}
}
六、最佳实践总结
CORS最佳实践
- 开发环境:允许localhost和常用内网IP
- 生产环境:精确配置允许的域名
- 安全考虑:限制Methods和Headers
- 性能优化:合理设置maxAge减少预检请求
Redis最佳实践
- 键命名规范 :使用冒号分隔,如
user:123:profile
- 过期时间:必须设置,避免内存泄漏
- 序列化选择:JSON序列化便于调试和跨语言
- 异常处理:缓存异常不应影响主业务
- 监控告警:监控命中率、内存使用、慢查询
- 缓存策略 :
- Cache Aside:适合大多数场景
- Read Through:适合读多写少
- Write Through:适合数据一致性要求高
- Write Behind:适合写入性能要求高
这就是从入门到精通的跨域和Redis缓存完整指南!掌握这些知识,你就能构建高性能、高可用的分布式应用系统。