From bb80b6da7a0a0175206e035132864cc808717614 Mon Sep 17 00:00:00 2001 From: fang <2627729817@qq.com> Date: Tue, 18 Mar 2025 15:41:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(cache):=20=E6=B7=BB=E5=8A=A0=E5=BA=97?= =?UTF-8?q?=E9=93=BA=E6=9C=8D=E5=8A=A1=E7=9A=84=20Redis=20=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RedisIdWorker 工具类,用于生成分布式 ID - 在 ShopServiceImpl 中基于互斥锁解决缓存击穿问题 - 添加测试用例验证 ID 生成和缓存功能 --- .../hmdp/service/impl/ShopServiceImpl.java | 73 ++++++++++++++++++- .../java/com/hmdp/utils/RedisIdWorker.java | 31 ++++++++ .../com/hmdp/HmDianPingApplicationTests.java | 22 +++++- 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/hmdp/utils/RedisIdWorker.java diff --git a/src/main/java/com/hmdp/service/impl/ShopServiceImpl.java b/src/main/java/com/hmdp/service/impl/ShopServiceImpl.java index 31d0f47..3d17787 100644 --- a/src/main/java/com/hmdp/service/impl/ShopServiceImpl.java +++ b/src/main/java/com/hmdp/service/impl/ShopServiceImpl.java @@ -1,5 +1,7 @@ package com.hmdp.service.impl; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; @@ -32,6 +34,14 @@ public class ShopServiceImpl extends ServiceImpl implements IS private StringRedisTemplate stringRedisTemplate; @Override public Result queryById(Long id) { + Shop shop = queryByIdWithMutex(id); + if (shop == null){ + return Result.fail("店铺不存在"); + } + return Result.ok(shop); + } + + public Shop queryByIdWithRedis(Long id) { // 使用redis缓存 // 查询缓存 String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); @@ -40,16 +50,73 @@ public class ShopServiceImpl extends ServiceImpl implements IS if(StrUtil.isNotBlank(cacheShop)){ // 存在直接返回 shop = JSONUtil.toBean(cacheShop, Shop.class); - return Result.ok(shop); + return shop; } // 不存在,查询数据库 shop = getById(id); if(shop == null){ // 数据库不存在,返回错误 - return Result.fail("店铺不存在"); + return null; + // 解决缓存穿透的话,就在这往redis存一个null } stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); - return Result.ok(shop); + return shop; + } + + public Shop queryByIdWithMutex(Long id) { + String key = CACHE_SHOP_KEY + id; + // 使用redis缓存 + // 查询缓存 + String cacheShop = stringRedisTemplate.opsForValue().get(key); + // 判断是否存在 + Shop shop = new Shop(); + if(StrUtil.isNotBlank(cacheShop)){ + // 存在直接返回 + shop = JSONUtil.toBean(cacheShop, Shop.class); + return shop; + } + + boolean isLock = false; + try { + isLock =tryLock(id); + // 不存在,尝试获取互斥锁 + if(isLock){ + // 获取到互斥锁,查询数据库 + shop = getById(id); + Thread.sleep(200); //模拟重建时的延迟 + if(shop == null){ + // 解决缓存穿透的话,就在这往redis存一个null + stringRedisTemplate.opsForValue().set(key, "",CACHE_SHOP_TTL, TimeUnit.MINUTES); + // 数据库不存在,返回错误 + return null; + } + // 存入缓存 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); + } + // 没有获取到互斥锁 + // 休眠后重新执行 + Thread.sleep(50); + return queryByIdWithMutex (id); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + // 释放互斥锁 + if (isLock) { + delectLock(id); + } + + } + } + + private boolean tryLock(Long id) { + // 本质就是设置一个redis缓存,key任意 + // 设置10秒过期时间防止死锁,10秒就够,因为这些请求一半都很快 + Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock:shop:"+id, "1",10, TimeUnit.SECONDS); + return BooleanUtil.isTrue(flag); + } + private void delectLock(Long id) { + // 本质就是删除上面key对应的缓存 + stringRedisTemplate.delete("lock:shop:"+id); } @Override diff --git a/src/main/java/com/hmdp/utils/RedisIdWorker.java b/src/main/java/com/hmdp/utils/RedisIdWorker.java new file mode 100644 index 0000000..7a4ce0a --- /dev/null +++ b/src/main/java/com/hmdp/utils/RedisIdWorker.java @@ -0,0 +1,31 @@ +package com.hmdp.utils; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +@Component +public class RedisIdWorker { + private static final long BEGIN_TIMESTAMP = 1640995200L; + private static final long COUNT_BITS = 32; + @Resource + private StringRedisTemplate stringRedisTemplate; + // 拼接一个时间戳,再加一个根据输入的字符串自增长的32位id + public long nextId(String prefix) { + LocalDateTime now = LocalDateTime.now(); + long GapSecond = now.toEpochSecond(ZoneOffset.UTC)-BEGIN_TIMESTAMP; + String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + long count = stringRedisTemplate.opsForValue().increment("icr:" + prefix+date); + return GapSecond <<32 | count; + } + + +// public static void main(String[] args) { +// LocalDateTime localDateTime = LocalDateTime.of(2022, 1, 1, 0, 0, 0); +// System.out.println(localDateTime.toEpochSecond(ZoneOffset.UTC)); //获取这个时间的秒数 +// } +} diff --git a/src/test/java/com/hmdp/HmDianPingApplicationTests.java b/src/test/java/com/hmdp/HmDianPingApplicationTests.java index bf82f64..5f99c6f 100644 --- a/src/test/java/com/hmdp/HmDianPingApplicationTests.java +++ b/src/test/java/com/hmdp/HmDianPingApplicationTests.java @@ -1,9 +1,27 @@ package com.hmdp; +import com.hmdp.utils.RedisIdWorker; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -@SpringBootTest -class HmDianPingApplicationTests { +import javax.annotation.Resource; +@SpringBootTest() +@RunWith(SpringJUnit4ClassRunner.class) +public class HmDianPingApplicationTests { + @Autowired + RedisIdWorker redisIdWorker; + + @Test + public void testIdWorker() { + for (int i = 0; i < 100; i++) { + long id = redisIdWorker.nextId("order"); + System.out.println("id = " + id); + } + } + }