首页 / 资讯中心 / 文章详情

Redis高并发优化与分布式锁实战解析

Redis高并发优化与分布式锁实战解析 ★ FEATURED ARTICLE
1. 项目概述与核心架构解析黑马点评是一个典型的本地生活服务平台项目核心解决商户发券和用户秒杀探店场景下的高并发问题。作为开发者我在两个月内独立完成了这个前后端分离架构的项目。前端采用Vue框架实现响应式界面后端基于SpringBoot构建单体服务通过Nginx实现反向代理和负载均衡。数据存储方面MySQL负责持久化存储Redis则承担了缓存、分布式锁和高频数据存储等关键角色。这个项目的技术选型经过了深思熟虑SpringBoot提供了快速开发的能力Nginx的负载均衡特性有效分摊了单节点压力Redis的多功能特性完美匹配了高并发场景需求。特别值得一提的是在商户发券和用户秒杀这两个核心业务场景中通过Redis实现的分布式锁和缓存优化将接口响应时间从最初的500ms优化到了100ms以内性能提升显著。提示面试时被问及项目开发情况切忌说跟着教程做的。即使是在学习项目也要重点突出自己的优化思路和问题解决过程这能体现你的工程思维和解决问题的能力。2. 核心技术实现与优化方案2.1 Nginx配置与负载均衡策略在实际部署中我为项目配置了Nginx反向代理和负载均衡。具体配置如下upstream backend { server 192.168.1.101:8080; server 192.168.1.102:8080; } server { listen 80; server_name api.dianping.com; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ~* \.(jpg|jpeg|png|gif|css|js)$ { expires 7d; add_header Cache-Control public; } }选择轮询策略作为负载均衡算法主要基于两点考虑一是实现简单适合初期项目规模二是能均匀分配请求到各后端节点。同时我还配置了静态资源缓存将前端图片、JS/CSS文件缓存7天这使页面加载速度提升了约40%。2.2 Redis集群模式下的Session共享传统Tomcat Session复制方案在集群环境下存在严重性能问题。我的解决方案是用户登录成功后生成唯一token作为Redis key用户信息序列化后存入Redis设置30分钟过期时间前端在请求头中携带token后端拦截器从Redis获取用户信息并存入ThreadLocal// 登录成功生成token String token UUID.randomUUID().toString(); redisTemplate.opsForValue().set(user:session:token, JSON.toJSONString(user), 30, TimeUnit.MINUTES); // 拦截器验证逻辑 String token request.getHeader(Authorization); if(StringUtils.isNotBlank(token)){ String userJson redisTemplate.opsForValue().get(user:session:token); if(userJson ! null){ User user JSON.parseObject(userJson, User.class); UserContext.setUser(user); // 存入ThreadLocal } }这种方案相比Tomcat Session复制性能提升显著且扩展性强新增节点无需额外配置。2.3 登录校验与ThreadLocal应用拦截器ThreadLocal的方案实现了高效的登录校验和用户信息传递preHandle方法中校验登录状态校验通过后将用户信息存入ThreadLocalController和Service层直接从ThreadLocal获取用户信息afterCompletion中清理ThreadLocal防止内存泄漏public class UserInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 登录校验逻辑 User user checkLogin(request); UserContext.setUser(user); // 存入ThreadLocal return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.removeUser(); // 必须清理 } } // 上下文工具类 public class UserContext { private static final ThreadLocalUser userHolder new ThreadLocal(); public static void setUser(User user) { userHolder.set(user); } public static User getUser() { return userHolder.get(); } public static void removeUser() { userHolder.remove(); // 防止内存泄漏 } }注意ThreadLocal使用后必须remove否则会导致用户信息错乱和内存泄漏。Tomcat使用线程池会复用线程如果不清理后续请求可能获取到前一个请求的用户信息。3. Redis缓存问题解决方案3.1 缓存穿透防护策略缓存穿透是指查询不存在的数据导致请求直接打到数据库。我采用了双重防护空值缓存对查询结果为null的key设置短过期时间60秒布隆过滤器预热有效商户ID请求先经过布隆过滤器校验// 空值缓存实现 public Shop getShopById(Long id) { String key cache:shop: id; // 1. 从缓存查询 String shopJson redisTemplate.opsForValue().get(key); if (shopJson ! null) { if (.equals(shopJson)) { // 空值标识 return null; } return JSON.parseObject(shopJson, Shop.class); } // 2. 查询数据库 Shop shop shopMapper.selectById(id); if (shop null) { // 空值写入缓存 redisTemplate.opsForValue().set(key, , 60, TimeUnit.SECONDS); return null; } // 3. 写入缓存 redisTemplate.opsForValue().set(key, JSON.toJSONString(shop), 30, TimeUnit.MINUTES); return shop; }布隆过滤器适合防御恶意攻击而空值缓存则能处理正常的无效查询。3.2 缓存击穿应对方案针对热点key突然失效的问题我根据业务场景采用了两种方案互斥锁方案适合对一致性要求高的普通业务逻辑过期方案适合秒杀等高并发场景互斥锁实现public Shop getShopWithLock(Long id) { String key cache:shop: id; // 1. 查询缓存 String shopJson redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(shopJson)) { return JSON.parseObject(shopJson, Shop.class); } // 2. 获取锁 String lockKey lock:shop: id; boolean locked false; try { locked tryLock(lockKey); if (!locked) { Thread.sleep(50); return getShopWithLock(id); // 重试 } // 3. 再次检查缓存双检 shopJson redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(shopJson)) { return JSON.parseObject(shopJson, Shop.class); } // 4. 查询数据库 Shop shop shopMapper.selectById(id); if (shop null) { redisTemplate.opsForValue().set(key, , 60, TimeUnit.SECONDS); return null; } // 5. 写入缓存 redisTemplate.opsForValue().set(key, JSON.toJSONString(shop), 30, TimeUnit.MINUTES); return shop; } finally { if (locked) { unlock(lockKey); } } }逻辑过期方案Data public class RedisData { private LocalDateTime expireTime; private Object data; } public Shop getShopWithLogicalExpire(Long id) { String key cache:shop: id; // 1. 查询缓存 String json redisTemplate.opsForValue().get(key); if (StringUtils.isBlank(json)) { return null; } // 2. 解析数据 RedisData redisData JSON.parseObject(json, RedisData.class); Shop shop JSON.parseObject(JSON.toJSONString(redisData.getData()), Shop.class); // 3. 判断是否过期 if (redisData.getExpireTime().isAfter(LocalDateTime.now())) { return shop; } // 4. 过期则获取锁重建 String lockKey lock:shop: id; boolean locked tryLock(lockKey); if (locked) { // 5. 开启独立线程重建缓存 CACHE_REBUILD_EXECUTOR.submit(() - { try { this.saveShopToRedis(id, 30L); } finally { unlock(lockKey); } }); } return shop; }3.3 缓存雪崩预防措施为防止大量key同时失效我实施了以下策略随机过期时间基础过期时间随机值0-5分钟Redis主从部署避免单点故障服务降级核心接口熔断保护数据库// 设置随机过期时间 long expireTime 30 new Random().nextInt(5); // 30-35分钟 redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);4. 分布式锁深度解析4.1 Redisson分布式锁实现原理Redisson的分布式锁解决了原生Redis锁的多个痛点可重入性同一线程可多次获取锁自动续期WatchDog机制防止业务未完成锁过期高可用支持Redisson的多种集群模式WatchDog工作机制默认锁过期时间30秒看门狗每10秒检查一次过期时间的1/3如果线程仍持有锁则续期到30秒业务完成释放锁时取消续期// Redisson锁使用示例 RLock lock redissonClient.getLock(order:lock: orderId); try { // 尝试加锁最多等待100秒上锁后30秒自动解锁 boolean res lock.tryLock(100, 30, TimeUnit.SECONDS); if (res) { // 业务逻辑 } } finally { lock.unlock(); }4.2 原生Redis锁实现与局限不使用Redisson时可用以下方式实现基本分布式锁-- 加锁脚本 if redis.call(setnx, KEYS[1], ARGV[1]) 1 then return redis.call(expire, KEYS[1], ARGV[2]) else return 0 end -- 解锁脚本 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这种实现存在三个主要问题锁续期需要自行实现不支持可重入主从切换可能导致锁丢失4.3 秒杀场景锁优化实践在优惠券秒杀场景中我采用了多级防护前端限流按钮10秒内只能点击一次Redis拦截分布式锁防止重复下单Lua脚本原子扣减库存数据库兜底唯一索引防止重复订单乐观锁防止超卖public Result seckillVoucher(Long voucherId) { // 1. 查询优惠券 String stockKey seckill:stock: voucherId; // 2. 判断库存Redis预检 Integer stock (Integer) redisTemplate.opsForValue().get(stockKey); if (stock null || stock 0) { return Result.fail(库存不足); } // 3. 一人一单检查 Long userId UserContext.getUser().getId(); String lockKey lock:order: voucherId : userId; boolean locked false; try { locked tryLock(lockKey); if (!locked) { return Result.fail(不允许重复下单); } // 4. 扣减库存Lua保证原子性 String script if tonumber(redis.call(get, KEYS[1])) 0 then redis.call(decr, KEYS[1]); return 1; else return 0; end; Long result redisTemplate.execute( new DefaultRedisScript(script, Long.class), Collections.singletonList(stockKey) ); if (result 0) { return Result.fail(库存不足); } // 5. 创建订单异步 orderQueue.add(new OrderMessage(voucherId, userId)); return Result.ok(下单成功); } finally { if (locked) { unlock(lockKey); } } }5. 高级功能实现与性能优化5.1 ZSet实现点赞排行榜使用Redis的ZSet实现点赞排行榜具有极佳性能// 点赞实现 public void likeBlog(Long blogId) { Long userId UserContext.getUser().getId(); String key blog:liked: blogId; Double score redisTemplate.opsForZSet().score(key, userId.toString()); if (score null) { // 未点赞执行点赞 redisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); // 点赞数1 redisTemplate.opsForHash().increment(blog:stats, blogId.toString(), 1); } else { // 已点赞取消点赞 redisTemplate.opsForZSet().remove(key, userId.toString()); // 点赞数-1 redisTemplate.opsForHash().increment(blog:stats, blogId.toString(), -1); } } // 分页查询排行榜 public ListBlog queryHotBlog(Integer current) { String key blog:liked:rank; // 按点赞数降序分页查询 SetString blogIds redisTemplate.opsForZSet().reverseRange( key, (current - 1) * 10, current * 10 - 1); // 查询博客详情 return blogMapper.selectBatchIds(blogIds); }对于大数据量分页建议使用游标分页替代传统分页// 游标分页实现 public PageResult queryBlogByScore(Double lastScore, Integer offset) { String key blog:liked:rank; // 按score范围查询 SetZSetOperations.TypedTupleString tuples redisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, lastScore, offset, 10); // 处理结果... }5.2 Set实现共同关注共同关注功能利用Set的交集操作实现高效查询// 关注用户 public void followUser(Long followUserId) { Long userId UserContext.getUser().getId(); String key user:follow: userId; redisTemplate.opsForSet().add(key, followUserId.toString()); } // 取消关注 public void unfollowUser(Long followUserId) { Long userId UserContext.getUser().getId(); String key user:follow: userId; redisTemplate.opsForSet().remove(key, followUserId.toString()); } // 查询共同关注 public ListUser commonFollow(Long userId1, Long userId2) { String key1 user:follow: userId1; String key2 user:follow: userId2; // 计算交集 SetString userIds redisTemplate.opsForSet().intersect(key1, key2); // 查询用户详情 return userMapper.selectBatchIds(userIds); }5.3 压测与性能优化通过JMeter压测核心接口优化前后对比如下指标优化前优化后提升幅度秒杀接口QPS2001500650%平均响应时间500ms100ms80%超卖发生率5%0%100%优化措施包括库存预热提前加载到Redis异步下单RabbitMQ削峰填谷多级缓存减少数据库访问分布式锁Redisson替代MySQL乐观锁SQL优化合理索引避免全表扫描// 异步下单实现 RabbitListener(queues order.queue) public void listenOrderQueue(OrderMessage message) { try { // 1. 创建订单 Order order createOrder(message); // 2. 扣减库存 int success voucherMapper.updateStock(message.getVoucherId()); if (success 0) { log.error(库存不足订单创建失败); return; } // 3. 保存订单 orderMapper.insert(order); } catch (Exception e) { log.error(订单处理异常, e); } }6. 数据一致性保障方案6.1 缓存与数据库一致性采用延迟双删策略保证最终一致性更新数据库删除缓存延迟一定时间后再次删除缓存兜底public void updateShop(Shop shop) { // 1. 更新数据库 shopMapper.updateById(shop); // 2. 删除缓存 redisTemplate.delete(cache:shop: shop.getId()); // 3. 延迟双删 delayDoubleDelete(cache:shop: shop.getId()); } private void delayDoubleDelete(String key) { // 5秒后执行第二次删除 CompletableFuture.runAsync(() - { try { Thread.sleep(5000); redisTemplate.delete(key); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); }对于秒杀库存等关键数据额外采用Canal监听binlog的方案Canal伪装成MySQL从库解析binlog中的库存变更同步删除Redis中的库存缓存6.2 分布式事务处理在跨服务操作时采用最终一致性方案本地消息表记录事务状态定时任务补偿未完成的事务幂等设计防止重复处理// 下单事务示例 Transactional public Result createOrder(OrderDTO orderDTO) { // 1. 扣减库存 boolean success seckillVoucher(orderDTO.getVoucherId()); if (!success) { throw new RuntimeException(库存不足); } // 2. 创建订单 Order order new Order(); // 设置订单属性... orderMapper.insert(order); // 3. 发送消息本地事务表 mqSender.send(new OrderMessage(order.getId(), orderDTO)); return Result.ok(order.getId()); }7. 项目经验与问题排查7.1 最难解决的问题分布式锁提前释放在初期压测时遇到的超卖问题排查过程如下现象同一时间出现多个相同订单日志分析发现锁释放后业务仍在执行定位原因业务执行时间超过锁过期时间解决方案采用Redisson WatchDog自动续期优化业务流程缩短锁持有时间非核心操作改为异步执行// 优化后的秒杀流程 public Result seckillVoucher(Long voucherId) { // 1. 快速校验库存、用户资格等 // 2. 获取Redisson锁带WatchDog RLock lock redissonClient.getLock(order:lock: voucherId : userId); try { if (!lock.tryLock(1, 10, TimeUnit.SECONDS)) { return Result.fail(操作太频繁); } // 3. 核心校验二次检查 // 4. 扣减Redis库存 // 5. 发送异步下单消息 return Result.ok(下单成功); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }7.2 性能优化心得缓存设计原则高频读、低频写的数据适合缓存缓存命中率要监控过低说明策略有问题缓存key设计要有规律方便批量操作锁使用注意事项锁粒度要尽可能细锁持有时间要尽可能短必须考虑锁释放的可靠性数据库优化经验索引不是越多越好要针对查询场景大表查询必须带索引条件复杂查询考虑拆分为多个简单查询7.3 项目扩展思考这个项目还有以下可优化方向服务拆分将单体架构拆分为微服务读写分离MySQL主从架构减轻读压力分库分表订单表按用户ID分片多级缓存加入本地缓存Caffeine限流熔断使用Sentinel保护核心接口在实际开发中我深刻体会到技术选型需要权衡各种因素。比如Redis虽然快但不能完全替代MySQL分布式锁虽然强大但会增加系统复杂度。每个技术决策都应该基于具体的业务场景和性能需求。
阅读完成 · 觉得有帮助?
咨询建站