编辑
2026-03-10
java
00

目录

1.互斥锁解决缓存击穿问题(setnx简单实践)
2.逻辑过期解决缓存击穿
3.商品秒杀业务
4.集群模式下的分布式锁
5.Lua脚本实现多条命令的原子性问题
6.Redisson实现的分布式锁
redisson的multiLock (分布式锁的主从一致性问题)
redis秒杀优化
redis实现消息队列
基于list实现消息队列
基于PubSub实现的消息队列

1.互斥锁解决缓存击穿问题(setnx简单实践)

数据一致性高,并发能力弱

image.png

代码实践

java
/** * 加redis分布式锁 * @param key * @return */ private boolean tryLock(String key){ Boolean status = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS); return BooleanUtil.isTrue(status); } /** * 释放redis分布式锁 * @param key */ private void unlock(String key){ stringRedisTemplate.delete(key); }
java
/** * 互斥锁解决缓存击穿 * @param id * @return */ private Shop queryWithMutex(Long id) { String lockKey = RedisConstants.LOCK_SHOP_KEY+id; Shop shop = null; // 查询redis是否有数据 String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); // 判断是否存在 if (StrUtil.isNotBlank(shopJson)){ // 获取shop对象 // 直接返回商铺信息 return JSONUtil.toBean(shopJson, Shop.class); } try { // 缓存未命中获取互斥锁 // 互斥锁获取成功 boolean isLock = tryLock(lockKey); if (!isLock){ // 互斥锁获取失败休眠并重试 Thread.sleep(50); return queryWithMutex(id); } // 不存在查询数据库 shop = getById(id); if (shop == null){ // 存储空值避免缓存穿透 stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 写入redis stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); }finally { // 释放锁 unlock(lockKey); } // 返回店铺信息 return shop; }

2.逻辑过期解决缓存击穿

数据一致性低,并发能力强

image.png

java
/** * reids逻辑过期方案 * @param id * @return */ public Shop queryWithLogicalExpire( Long id){ // 查询redis是否有数据 String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); // 判断是否存在 if (StrUtil.isBlank(shopJson)){ // 未命中缓存直接返回空 return null; } // 命中需要将json反序列化为对象 RedisData bean = JSONUtil.toBean(shopJson, RedisData.class); Shop shop = JSONUtil.toBean((JSONObject) bean.getData(), Shop.class); LocalDateTime expireTime = bean.getExpireTime(); // 判断是否过期 if (expireTime.isAfter(LocalDateTime.now())){ // 未过期 return shop; } // 已过期开启缓存重建 // 获取互斥锁 String lockKey = RedisConstants.LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); if (isLock){ // 获取锁成功开启独立线程缓存重建 CACHE_REBUID_EXECUTOR.submit(()->{ try { // 重建缓存 this.saveShop2Redis(id,20L); } catch (Exception e) { throw new RuntimeException(e); }finally { // 释放锁 unlock(lockKey); } }); } // 返回旧数据 return shop; }

3.商品秒杀业务

代码实践

java
/** * 优惠券秒杀实现 * * @param voucherId * @return */ @Override public Result seckillVoucher(Long voucherId) { // 查询优惠券 SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId); // 判断是否开始秒杀 if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("优惠券秒杀尚未开始"); } // 判断秒杀是否结束 if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("优惠券秒杀已经结束"); } // 判断库存是否充足 if (seckillVoucher.getStock() < 1){ return Result.fail("优惠券库存不足"); } Long userId = UserHolder.getUser().getId(); // 悲观锁实现一人一券 synchronized (userId.toString().intern()) { // 防止事务失效使用代理对象 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder(Long voucherId) { // 一人一单判断 // 查询id Long id = UserHolder.getUser().getId(); int count = query().eq("user_id", id).eq("voucher_id", voucherId).count(); if (count > 0){ // 说明用户已经购买过 return Result.fail("用户已经购买过一张"); } // 扣减库存 乐观锁 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock",0) // 如果stock和之前未修改时一样说明不存在数据被修改 .update(); if (!success){ return Result.fail("优惠券库存不足"); } // 创建订单 VoucherOrder voucherOrder = new VoucherOrder(); // 订单id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); // 用户id voucherOrder.setUserId(UserHolder.getUser().getId()); // 订单id voucherOrder.setVoucherId(voucherId); // 保存订单 save(voucherOrder); return Result.ok(orderId); }

使用乐观锁实现扣减库存 使用悲观锁实现一人一单的判断

4.集群模式下的分布式锁

image.png

redis实现分布式锁

image.png

初级版本

java
package com.hmdp.utils; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate stringRedisTemplate; private static final String KEY_PREFIX = "lock:"; public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) { this.stringRedisTemplate = stringRedisTemplate; this.name = name; } /** * 尝试上锁 * * @param timeout * @return */ @Override public boolean tryLock(long timeout) { // 获取线程显示 long id = Thread.currentThread().getId(); // 获取锁 Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name,id+"",timeout, TimeUnit.SECONDS); // 如果不存在才执行 return Boolean.TRUE.equals(success); } /** * 释放锁 */ @Override public void unlock() { stringRedisTemplate.delete(KEY_PREFIX + name); } }

redis锁误删问题

image.png

当线程获取锁后业务阻塞执行超时 锁触发超时释放 其他线程获取到锁 但阻塞线程执行完毕执行解锁 将后来线程的锁解除 导致多个线程同时获取到锁

在释放锁时进行锁标识符(UUID 线程id)判断

5.Lua脚本实现多条命令的原子性问题

使用lua脚本实现redis命令的原子性

lua
--比较线程标示与锁中的标示是否一致 if (redis.call('get',KEYS[1] == ARGV[1])) then --释放锁 return redis.call('del',KEYS[1]) end return 0
java
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; // 初始化文件中的lua脚本 static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); }
java
/** * 解锁 */ @Override public void unlock() { stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name), ID_PREFIX+Thread.currentThread().getId() ); }

6.Redisson实现的分布式锁

setnx实现的分布式锁的问题

image.png

image.png

导入redisson

配置redisson

java
package com.hmdp.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedisConfig { @Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.42.20:6379").setPassword("123456"); return Redisson.create(config); } }

修改业务

java
RLock lock = redissonClient.getLock("order" + userId); // SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order" + userId); // 获取锁 boolean isLock = lock.tryLock(); // 判断获取锁是否成功 if (!isLock){ // 获取锁失败,返回错误或重试 return Result.fail("不允许重复下单"); } try { // 防止事务失效使用代理对象 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }finally { lock.unlock(); }

可重入锁原理

image.png

lua脚本实践

image.png

redisson的multiLock (分布式锁的主从一致性问题)

image.png

配置3个redis

添加联锁

java
redissonClient.getMultiLock(redissonClient,redissonClient2,redissonClient3);

redis秒杀优化

image.png

编写lua脚本实现购票判断

lua
--1.参数列表 --1.1优惠券id local voucherId = ARGV[1] --用户id local userId = ARGV[2] local stockKey = 'seckill:stock:'..voucherId local orderKey = 'seckill:order:'..voucherId if (tonumber(redis.call('get',stockKey))<=0) then --库存不足 return 1; end --判断用户是否下单 if (redis.call('sismember',orderKey,userId) == 1) then return 2 end --扣减库存 redis.call('incrby',stockKey,-1) --下单 redis.call('sadd',orderKey,userId) return 0

使用阻塞队列实现异步添加订单

java
/** * 优惠券秒杀实现 * * @param voucherId * @return */ @Override public Result seckillVoucher(Long voucherId) { SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId); // 判断是否开始秒杀 if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("优惠券秒杀尚未开始"); } // 判断秒杀是否结束 if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("优惠券秒杀已经结束"); } // 使用redis优化秒杀逻辑 // 执行lua脚本 Long result = stringRedisTemplate.execute( SECKILL_SCRIPT , Collections.emptyList() , voucherId.toString() , UserHolder.getUser().getId().toString() ); // 判断返回值 int r = result.intValue(); if (r != 0){ // 不为0代表没有购买资格 return Result.fail(r == 1 ? "库存不足" : "不能重复下单"); } // 为0有购买资格 VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); // 保存到阻塞队列 // 用户id voucherOrder.setId(orderId); // 代金券id voucherOrder.setVoucherId(voucherId); // 放入到阻塞队列中 orderTasks.add(voucherOrder); // 获取代理对象 proxy = (IVoucherOrderService) AopContext.currentProxy(); // 返回订单id return Result.ok(orderId); }

redis实现消息队列

基于list实现消息队列

使用redis中的brpop和lpush实现

image.png

基于PubSub实现的消息队列

image.png

本文作者:钱小杰

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!