跨域(CORS)和缓存中间件(Redis)深度解析

跨域(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️⃣ 预检缓存
    }
}

配置项详解

  1. addMapping("/")**

    • /**:对所有API路径生效
    • /api/**:只对/api开头的路径生效
  2. allowedOriginPatterns

    • 支持通配符模式(Spring 5.3+)
    • allowedOrigins更灵活
  3. allowedMethods

    • 简单方法:GET、POST、HEAD
    • 需预检的方法:PUT、DELETE、PATCH
  4. allowCredentials(true)

    • 允许发送Cookie
    • 注意:不能与allowedOrigins("*")同时使用
  5. 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最佳实践
  1. 开发环境:允许localhost和常用内网IP
  2. 生产环境:精确配置允许的域名
  3. 安全考虑:限制Methods和Headers
  4. 性能优化:合理设置maxAge减少预检请求
Redis最佳实践
  1. 键命名规范 :使用冒号分隔,如 user:123:profile
  2. 过期时间:必须设置,避免内存泄漏
  3. 序列化选择:JSON序列化便于调试和跨语言
  4. 异常处理:缓存异常不应影响主业务
  5. 监控告警:监控命中率、内存使用、慢查询
  6. 缓存策略
    • Cache Aside:适合大多数场景
    • Read Through:适合读多写少
    • Write Through:适合数据一致性要求高
    • Write Behind:适合写入性能要求高

这就是从入门到精通的跨域和Redis缓存完整指南!掌握这些知识,你就能构建高性能、高可用的分布式应用系统。

相关推荐
波波烤鸭3 小时前
Redis 高可用实战源码解析(Sentinel + Cluster 整合应用)
数据库·redis·sentinel
MarkHard12311 小时前
如何利用redis使用一个滑动窗口限流
数据库·redis·缓存
island131413 小时前
【Redis#10】渐进式遍历 | 数据库管理 | redis_cli | RES
数据库·redis·bootstrap
心想事成的幸运大王13 小时前
Redis的过期策略
数据库·redis·缓存
wuyunhang12345618 小时前
Redis---集群模式
数据库·redis·缓存
Seven9719 小时前
Redis是如何进行内存管理的?缓存中有哪些常见问题?如何实现分布式锁?
redis
IAtlantiscsdn19 小时前
Redis Stack扩展功能
java·数据库·redis
没有bug.的程序员20 小时前
Redis 大 Key 与热 Key:生产环境的风险与解决方案
java·数据库·redis·缓存·热key·大key
wuyunhang12345620 小时前
Redis----缓存策略和注意事项
redis·缓存·mybatis