位图初始化sku信息到缓存中,如果商品有上下架,同步更新位图信息。使用商品id作为偏移量设置位图的true / false。
使用setnx + lua 脚本解决
@Autowired
private RedisTemplate redisTemplate;
/*
* 根据SkuID查询SKU商品信息
*
* @param skuId
* @return
*/
@Override
public ProductSku getProductSku(Long skuId) {
try {
//1.优先从缓存中获取数据
//1.1 构建业务数据Key 形式:前缀+业务唯一标识
String dataKey = "product:sku:" + skuId;
//1.2 查询Redis获取业务数据
ProductSku productSku = (ProductSku) redisTemplate.opsForValue().get(dataKey);
//1.3 命中缓存则直接返回
if (productSku != null) {
log.info("命中缓存,直接返回,线程ID:{},线程名称:{}", Thread.currentThread().getId(), Thread.currentThread().getName());
return productSku;
}
//2.尝试获取分布式锁(set k v nx ex 可能获取锁失败)
//2.1 构建锁key
String lockKey = "product:sku:lock:" + skuId;
//2.2 采用UUID作为线程标识
String lockVal = UUID.randomUUID().toString().replaceAll("-", "");
//2.3 利用Redis提供set nx ex 获取分布式锁
Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, lockVal, 5, TimeUnit.SECONDS);
if (flag) {
//3.获取锁成功执行业务,将查询业务数据放入缓存Redis
log.info("获取锁成功:{},线程名称:{}", Thread.currentThread().getId(), Thread.currentThread().getName());
try {
//再次检查缓存,是否有数据
productSku = (ProductSku)redisTemplate.opsForValue().get(dataKey);
if(productSku != null){
return productSku;
}
productSku = baseMapper.selectById(skuId);
//防止缓存穿透,将null值转换成新对象存入数据库,并且设置较短的过期时间
long ttl = productSku == null ? 1 * 60 : 10 * 60;
if(productSku == null){
productSku = new ProductSku();
}
//将查询数据库结果放入缓存
redisTemplate.opsForValue().set(dataKey, productSku, ttl, TimeUnit.SECONDS);
return productSku;
} finally {
//4.业务执行完毕释放锁
String scriptText = """
if redis.call('get',KEYS[1]) == ARGV[1]
then
return redis.call('del',KEYS[1])
else
return 0
end
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(scriptText);
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Arrays.asList(lockKey), lockVal);
}
} else {
try {
//5.获取锁失败则自旋(业务要求必须执行)
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.error("获取锁失败,自旋:{},线程名称:{}", Thread.currentThread().getId(), Thread.currentThread().getName());
return this.getProductSku(skuId);
}
} catch (Exception e) {
//兜底处理方案:Redis服务有问题,将业务数据获取自动从数据库获取
log.error("[商品服务]查询商品信息异常:{}", e);
return baseMapper.selectById(skuId);
}
}
购物车功能只将商品添加到redis缓存中。有以下几个功能,就是对redis的操作
构建支付模块,引入alipay-skd-java依赖,配置yml所需参数(区分正式环境/沙箱环境)
构建配置类,引入支付url,私钥,返回链接,通知链接,公钥,appid等参数,返回alipayClient组件
支付-Handler:根据订单号支付,传入订单号,调用服务层提交表单,我们返回给前端一个 String payForm
支付-Impl:先保存本地交易记录到数据库,然后创建支付API对应的请求对象,同步回调引导用户至成功页面,用户付款成功后,支付宝异步通知商户系统 ,设置setBizContent,调用客户端的pageExecute方法并返回String 类型 响应体
保存本地交易记录-Impl:传入订单号,根据订单号查询本地交易记录,如果存在则直接返回。然后构建本地交易记录的对象PaymentInfo ,其中需要远程调用根据订单号获取订单信息,如果订单已支付/已关闭直接返回,否则继续构建交易记录对象。构建完毕后,save并返回交易记录对象
支付回调-Handler:传入一个map,调用支付宝回调api,并为其配置白名单
支付回调-Impl:rsaCheckV1验证签名,setnx设置订单回调的信息缓存进行幂等性处理,再次验证回调信息的正确性,并更新本地交易记录的状态为已支付 updatePaymentStatus
支付回调-Config : 支付成功后,由于本地接口位于一个私有IP范围内,无法被公共网络访问这个时候我们需要内网穿透技术
更新本地交易记录Impl :传入的是map类型的支付宝回调信息,先验证用户支付金额与商户侧应付金额是否一致,如果不一致抛出异常。然后更新本地交易记录对象并重新保存,保存后发送修改订单状态以及库存扣减通知Msg到相应服务的交换机以及路由上。
修改订单状态-rabbit : 监听到支付成功的消息之后,调用spi将订单状态更新即可
扣减商品库存-rabbit :监听到成功消息后,调用扣减库存spi,setnx保证幂等性
扣减商品库存-Impl : 根据订单号获取 缓存中的锁定库存信息,skulist,并对每个sku库存
[Order Service]
│
└─→ (Feign) [Stock Service: lockStock]
│ │
│ └─→ DB悲观锁 → Redis缓存
│
└─→ [Alipay SDK] → 支付页面跳转
│
└─→ 支付成功 → 回调接口
│
└─→ 验签 → Redis幂等校验 → 更新支付状态
│
└─→ RabbitMQ → [Order Service: updateStatus]
│
└─→ [Stock Service: deductStock]
需要分类处理,但可通过注解和切面统一管理。**
缓存操作类型不同:
@Cacheable
):命中缓存则直接返回结果,未命中则执行方法并缓存。@CachePut
):无论是否命中缓存,都执行方法并将结果更新到缓存。@CacheEvict
):清空缓存中的特定键。allEntries
):清空所有缓存条目。业务逻辑差异:
查询和更新的缓存逻辑不同,分开处理能避免冲突(例如更新后需立即清空旧缓存)。
设计注解:
定义 @MyCacheable
、@MyCachePut
、@MyCacheEvict
等注解,分别对应不同操作类型。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCacheable {
String key();
long expireTime();
TimeUnit unit();
}
切面逻辑分离:
在切面中通过注解类型判断操作类型,并调用对应的缓存方法(如 get
、put
、delete
)。
@Around("@annotation(myCacheable)")
public Object handleCacheable(ProceedingJoinPoint pjp, MyCacheable myCacheable) {
// 查询缓存逻辑
}
@Around("@annotation(myCachePut)")
public Object handleCachePut(ProceedingJoinPoint pjp, MyCachePut myCachePut) {
// 更新缓存逻辑
}
需结合事务和缓存更新策略。**
双写策略:
数据库更新后,先更新数据库,再删除缓存。
@Transactional
public void updateData(User user) {
// 更新数据库
userRepository.save(user);
// 删除缓存
redisTemplate.delete("user:" + user.getId());
}
缓存过期时间分散:
为缓存设置随机过期时间(如 TTL + random(0,300s)
),避免同时过期。
redisTemplate.expire("user:" + id, expireTime + new Random().nextInt(300), TimeUnit.SECONDS);
热点数据预热:
对高频访问的数据(如商品详情页)在启动时预加载到缓存中。
需在缓存操作中加入幂等性控制。
Redis原子操作:
使用 SETNX
(SET if Not Exists)或 Lua 脚本实现原子性校验。
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent("lock:" + key, "1", 5, TimeUnit.SECONDS);
if (isLocked) {
// 执行业务逻辑
} else {
throw new RuntimeException("重复提交");
}
分布式锁优化:
使用 Redlock 算法或 Redisson 的分布式锁应对高并发场景。
合理使用 EL 表达式可提高灵活性,但需避免过度复杂化。
user:#id
、order:#userId:#orderId
)。SpEL 表达式解析:
在切面中通过 SpelExpressionParser
解析注解中的表达式,并绑定方法参数。
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
context.setVariable("arg" + i, args[i]);
}
String key = parser.parseExpression("#{arg0.id}").getValue(context, String.class);
避免冗余:
若表达式过于复杂(如嵌套调用),可能导致性能问题或维护困难。建议保持简洁,必要时拆分逻辑。
通过 SpEL 表达式提取对象属性,或手动序列化对象。
User user
),需提取 user.id
作为缓存键的一部分。SpEL 提取属性:
使用 #root.args[0].id
或 #user.id
动态获取对象属性。
@MyCacheable(key = "#user.id")
public User getUserById(User user) {
return userRepository.findById(user.getId());
}
手动序列化:
将对象转为 JSON 字符串(如 JSON.toJSONString(user)
),但需注意对象的 equals
和 hashCode
实现。
String key = "user:" + JSON.toJSONString(user);
工具类辅助:
使用 ObjectUtils
工具类提取关键字段(如 getId()
)。
String key = "user:" + user.getId();
问题 | 解决方案 |
---|---|
1. 分类插件 | 使用不同注解(@MyCacheable 、@MyCachePut )区分操作类型 |
2. 缓存一致性 | 双写策略 + 随机 TTL + 预热 |
3. 幂等性 | Redis 原子操作 + 分布式锁 |
4. EL 表达式 | SpEL 解析参数 + 避免复杂表达式 |
5. 对象参数 Key | SpEL 提取属性 + 手动序列化 |