feat(cache): 添加店铺服务的 Redis 缓存功能

- 新增 RedisIdWorker 工具类,用于生成分布式 ID
- 在 ShopServiceImpl 中基于互斥锁解决缓存击穿问题
- 添加测试用例验证 ID 生成和缓存功能
This commit is contained in:
fang 2025-03-18 15:41:31 +08:00
parent 5c962b28e6
commit bb80b6da7a
3 changed files with 121 additions and 5 deletions

View File

@ -1,5 +1,7 @@
package com.hmdp.service.impl; 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.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result; import com.hmdp.dto.Result;
@ -32,6 +34,14 @@ public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IS
private StringRedisTemplate stringRedisTemplate; private StringRedisTemplate stringRedisTemplate;
@Override @Override
public Result queryById(Long id) { 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缓存 // 使用redis缓存
// 查询缓存 // 查询缓存
String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
@ -40,16 +50,73 @@ public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IS
if(StrUtil.isNotBlank(cacheShop)){ if(StrUtil.isNotBlank(cacheShop)){
// 存在直接返回 // 存在直接返回
shop = JSONUtil.toBean(cacheShop, Shop.class); shop = JSONUtil.toBean(cacheShop, Shop.class);
return Result.ok(shop); return shop;
} }
// 不存在查询数据库 // 不存在查询数据库
shop = getById(id); shop = getById(id);
if(shop == null){ 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); 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 @Override

View File

@ -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)); //获取这个时间的秒数
// }
}

View File

@ -1,9 +1,27 @@
package com.hmdp; 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.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringBootTest import javax.annotation.Resource;
class HmDianPingApplicationTests {
@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);
}
}
} }