项目简介 1 2 星策味达是一款点餐系统,项目包含后台管理系统和客户端界面,实现了员工管理、菜品及套餐展示、 订单处理等核心功能,系统还引入了优惠券秒杀、菜品评价、附近美食等增值服务,丰富了用户的点餐体验。
延迟双删,缓存过期时间 数据一致性保障:采用延迟双删和缓存过期时间,巧妙地解决了数据不一致问题,防止了脏数据残留。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 1. 延迟双删 (1 ). 解决 “数据库更新期间,其他线程可能写入旧缓存” 的并发问题。 (2 ). 为什么需要第二次删除? 假设线程 A 删除缓存后,在更新数据库的过程中: 线程 B 查询数据,发现缓存已被删除,于是去数据库读取旧数据(此时线程 A 的更新尚未提交)。 线程 B 将旧数据写入缓存(导致缓存中再次出现旧数据)。 线程 A 完成数据库更新后,若不再次删除缓存,后续查询会读到线程 B 写入的旧数据(脏数据)。 第二次删除可清除线程 B 误写入的旧缓存,确保后续查询能读到数据库中的新数据并更新缓存。 (3 ). 第一次删除:打破 “旧缓存→旧数据” 的读取链路。 数据库更新:确保数据持久化到最新状态。 延迟第二次删除:清理并发场景下可能被写入的旧缓存,彻底断绝脏数据来源。 @Transactional public void updateUser (User user) { String cacheKey = USER_CACHE_KEY + user.getId(); redisTemplate.delete(cacheKey); userMapper.updateById(user); cacheUpdatePool.submit(() -> { try { TimeUnit.MILLISECONDS.sleep(500 ); redisTemplate.delete(cacheKey); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } 2. 缓存过期时间 (1 ). 创建实体类: @Data @AllArgsConstructor @NoArgsConstructor public class LogicalExpireCache <T> { private T data; private long expireTime; } (2 ). 再存入Redis时: private User queryDbAndWriteCache (Long id, String cacheKey) { User user = userMapper.selectById(id); if (user != null ) { long expireTime = System.currentTimeMillis() + LOGICAL_EXPIRE; LogicalExpireCache<User> cacheObj = new LogicalExpireCache <>(user, expireTime); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(cacheObj)); } return user; } (3 ). 查询时判断过期 public User getUserById (Long id) { String cacheKey = USER_CACHE_KEY + id; String cacheJson = redisTemplate.opsForValue().get(cacheKey); if (cacheJson == null ) { return queryDbAndWriteCache(id, cacheKey); } LogicalExpireCache<User> cacheObj = JSON.parseObject(cacheJson, new TypeReference <LogicalExpireCache<User>>() {}); User user = cacheObj.getData(); long expireTime = cacheObj.getExpireTime(); if (System.currentTimeMillis() < expireTime) { return user; } cacheUpdatePool.submit(() -> { updateCache(id, cacheKey); }); return user; } private User queryDbAndWriteCache (Long id, String cacheKey) { User user = userMapper.selectById(id); if (user != null ) { long expireTime = System.currentTimeMillis() + LOGICAL_EXPIRE; LogicalExpireCache<User> cacheObj = new LogicalExpireCache <>(user, expireTime); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(cacheObj)); } return user; }
秒杀(乐观锁和分布式锁) 优惠券秒杀活动优化:使用乐观锁和 Redission 分布式锁解决了秒杀活动中的商品超卖现象和一人一单的限制。使 用 Redis+RabbitMQ 对项目进行优化,系统吞吐量提升 50%1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 1. 乐观锁 @Update("UPDATE coupon SET stock = stock - 1, version = version + 1 " + "WHERE id = #{couponId} AND stock > 0 AND version = #{version}") int decreaseStockWithOptimisticLock ( @Param("couponId") Long couponId, @Param("version") Integer version) ;2. 分布式锁(用于防止一个用户多次下单) String lockKey = SECKILL_LOCK_KEY + couponId; RLock lock = redissonClient.getLock(lockKey); try { boolean isLocked = lock.tryLock(5 , 30 , TimeUnit.SECONDS); if (!isLocked) { throw new BusinessException ("购买失败" ); } ... } 3. 结合Redis和RabbitMQ处理下单解耦和防止一个人多个订单 (1 ). 若要使用RabbitMQ来解耦下单的过程。分布式锁就只隔离了消息发送这一阶段,若RabbitMQ卡住,可能会导致在判断当前用户是否已下单的时候出现错误导致出现一个人多个订单的情况。 (2 ). 使用Redis的SET NX 来标记用户是否已经下单。 String userFlagKey = USER_ORDER_FLAG_KEY + couponId + ":" + userId; Boolean isFirst = redisTemplate.opsForValue().setIfAbsent(userFlagKey, "1" , 10 , TimeUnit.MINUTES); if (Boolean.FALSE.equals(isFirst)) { throw new BusinessException ("购买失败" ); } ... 4. 优化:setNX + UserId作为分布式锁直接一步到位SET NX(用户粒度锁)更适合: 秒杀场景中 “一人一单” 的严格限制(锁粒度天然适配)。 业务逻辑简单、执行时间短且可控(无需复杂续期)。 追求极致性能,能接受手动处理锁细节。 团队对 Redis 命令熟悉,可规避锁释放、续期等潜在风险。 Redisson 更适合: 业务逻辑复杂、执行时间不确定(需自动续期)。 需高可用性(依赖 Redis 集群)和多种锁类型(如公平锁)。 团队更关注业务逻辑,希望减少分布式锁的底层编码工作。 系统中存在多种分布式并发场景(如秒杀、库存更新、分布式任务调度等),需统一锁框架。
为什么用websocket而不用接口 1 2 3 4 5 6 7 8 9 10 传统接口: 1. 无法主动推送数据2. 连接开销大3. 实时性差WebSocket: 1. 全双工实时通信:服务端可主动推数据2. 连接开销极低:一次握手,长期复用3. 低延迟:数据传输无 “请求等待”4. 服务端资源占用更优:减少无效连接
合适的字段建立索引和使用覆盖索引优化 SQL 查询性能,查询耗时减少 80% 1 2 3 4 5 6 7 8 1. 执行计划:EXPLAIN显示type = ALL(全表扫描),rows约 500 万(需扫描全表),Extra显示 “Using where; Using filesort”(需在内存 / 磁盘中排序);2. 对同一用户(user_id=12345 ,该用户有 100 条符合条件的订单)执行 10 次查询,取平均耗时为1500ms(1.5 秒),其中大部分时间用于全表扫描和文件排序;1. 索引设计:针对查询条件user_id和create_time,建立联合索引idx_user_create(user_id, create_time),且包含查询字段id, pay_amount(InnoDB 的二级索引会包含主键id,因此实际索引字段为(user_id, create_time, id, pay_amount));2. 覆盖索引效果:查询所需的id, create_time, pay_amount均可从索引中获取,无需回表查询主键索引(Extra显示 “Using index”),且ORDER BY create_time可利用索引的有序性,避免文件排序。1. 耗时统计:同样对user_id=12345 执行 10 次查询,平均耗时降至300ms;2. 性能提升计算:耗时从 1500ms 减少到 300ms,减少了 1200ms,计算得:(1500 -300 )/1500 = 80 %,即查询耗时减少 80 %。”