快速游览
- 缓存雪崩
- 缓存穿透
- 缓存击穿
- 封装工具类
黑马的视频讲的挺形象的我就套图了
因为程序原因或者突发事件导致宕机,大量缓存数据无法被击中,直接到数据库进行读写操作
解决方案:
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存穿透是指当用户在查询一条数据的时候,而此时数据库和缓存却没有关于这条数据的任何记录,而这条数据在缓存中没找到就会向数据库请求获取数据。用户拿不到数据时,就会一直发请求,查询数据库,这样会对数据库的访问造成很大的压力。
如:用户查询一个 id =aa的商品信息,一般数据库 id 值都是数字0开始自增,很明显这条信息是不在数据库中,当没有信息返回时,会一直向数据库查询,大量的io操作会给当前数据库的造成很大的访问压力。
一般项目的数据读写操作:
解决后思路 :
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 根据id查询商铺信息
* 添加redis缓存和数据库同步功能
* 1.更新数据时候 先更新数据库
* 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
* 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
*
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
//1.从redis缓存中查寻缓存
/**
* 因为这里使用的是forvalue操作对象
* 获取出来的对象是java字符串
* 所以我们需要反序列化成java对象
*/
String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
// 判断是否纯在 并且判断是否为空值缓存穿透策略
if (StrUtil.isNotBlank(s)){//该方法空值也会执行 肯定汇报错 所以写一个空值处理 空串会返回fasle 不执行if
//2.如果存在返回数据 先解析(反序列化)为java对象
Shop shop = JSONUtil.toBean(s, Shop.class);
return Result.ok(shop);
}
System.out.println(s);
// 判断是否是否为我们设置的防穿透值
if (s!=null) {//这里不能使用.equals("") 这样很会报错null指针异常
//或者s!=null 上一个if判断是否有值,这里不能是null null会去执行下一步
return Result.fail("店铺信息不存在");
}
/**
* 缓存没命中 查询数据库
*/
//3.不纯在则去mysql数据进行查找
Shop shop = getBaseMapper().selectById(id);
//4.mysql中查到
if (Objects.isNull(shop)){
//4.若在数据库中没有发现则返回错误 message
// 给redis 写"null" 防止有人恶意读写数据库(io)崩坏 并且数据自动销毁 避免管理员数据库更新数据后 而用户看到的还是空值
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,"",5,TimeUnit.MINUTES);//什么都不设是null ,''叫做empty
return Result.fail("没有该店铺信息");
}
//5.查到到对应数据即可返回 并且更新缓存区
// 字符串存储 存储java对象 需要转化为json字符串
String shopjson = JSONUtil.toJsonStr(shop);
//6. 设置缓存数据生命周期 自动删除 确保数据无论是否手动更新成功都清楚 让用户下次访问时写入缓存
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX+id,shopjson,30, TimeUnit.MINUTES);
return Result.ok(shop);
}
这里使用""表示数据库没有这个数据,所以如果缓存中命中的是该数据,直接返回错误提示信息
ps:,""是empty空字符和null不同
这里提一个点,.equals(“”),会把"“和null等同起来,使用的是hutool工具包也是,所以当我访问不纯在的信息时,redis查出数据是”",而传入该方法过后,报错空指针异常,
此外为了保证数据的一直性,当数据库发生更改,应删除对应数据,避免数据不一致
/**
* 更新数据库
* 添加redis缓存和数据库同步功能
* 1.更新数据时候 先更新数据库
* 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
* 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
*
* @return
*/
@Override
@Transactional//定位事务
public Result update(Shop shop) {
// 1.更新数据库
updateById(shop);
// 2.删除缓存
if (shop.getId()==null) {
return Result.fail("店铺id 不能为空");
}
stringRedisTemplate.delete(HmConstants.SHOP_PREX+shop.getId());
//3.将数据库操作和缓存操作设置为一个事务一个操作出错发生回滚 确保同步
return Result.ok();
}
发送测试:
redis结果:
还有中方式是使用布隆过滤器,但是布隆过滤器不会提供删除方法,在代码维护上比较困难。所以需要的伙伴可以自行了解
线程安全定义
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。
缓存击穿和雪崩类似 ,但是突然部分高并发,经常被擦查询的缓存数据丢失或者失效,无数个线程携带的海量请求,修改部分数据,是及其容易造成线程不安全,数据未知
互斥锁的工作原理,多个线程访问数据时,若缓存中未命中数据,进行判断,该线程是否拥有锁,拥有锁的权限即可查询数据库,对缓存进行重建,并且释放锁(避免死锁!!),没有获取到锁的线程经行睡眠,在重新尝试集中缓存
这么一描述,是不是感觉和redis有个指令很像,对就是setnx(k存在即不设置,不纯在k才能设置)
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 根据id查询商铺信息
* 添加redis缓存和数据库同步功能
* 1.更新数据时候 先更新数据库
* 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
* 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
*
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 1演示缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("没有店铺信息");
}
System.out.println(shop);
return Result.ok(shop);
}
/**
* 更新数据库
* 添加redis缓存和数据库同步功能
* 1.更新数据时候 先更新数据库
* 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
* 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
*
* @return
*/
@Override
@Transactional//定位事务
public Result update(Shop shop) {
// 1.更新数据库
updateById(shop);
// 2.删除缓存
if (shop.getId()==null) {
return Result.fail("店铺id 不能为空");
}
stringRedisTemplate.delete(HmConstants.SHOP_PREX+shop.getId());
//3.将数据库操作和缓存操作设置为一个事务一个操作出错发生回滚 确保同步
return Result.ok();
}
/**
* 尝试获取锁(模仿互斥锁)
*/
private boolean trylock(String id){
// 使用redis充当锁 setnx操作
String lockey = "lock:shop:";
// 当前线程如果没有锁则设置成功 返回true 有锁则返回false 设置到期时间防止锁在某个线程很久不结束
Boolean rflog = stringRedisTemplate.opsForValue().setIfAbsent(lockey+id, "huchilock", 5, TimeUnit.SECONDS);
return BooleanUtil.isTrue(rflog);//直接返回布尔值 实际调用拆箱(booleanvalue)方法 如果为null 会报指针异常
}
//释放锁 执行完防止缓存击穿过后把锁的数据删除
private void unlock(String id){
stringRedisTemplate.delete("lock:shop:"+id);
}
/* 将演示焕春击穿的代码封装成一个方法
在进行修改;便于保存
*/
public Shop queryWithMutex(Long id){
//1.从redis缓存中查寻缓存
/**
* 因为这里使用的是forvalue操作对象
* 获取出来的对象是java字符串
* 所以我们需要反序列化成java对象
*/
String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
// 判断是否命中 并且判断是否为空字符值缓存穿透策略
if (StrUtil.isNotBlank(s)){//该方法空值也会执行空字符和null返回false
不执行if
//2.如果存在返回数据 先解析(反序列化)为java对象
Shop shop = JSONUtil.toBean(s, Shop.class);
return shop;
}
// 判断是否是否为我们设置的防穿透值
if (s!=null) {//这里不能使用.equals("") 这样很会报错null指针异常
//或者s!=null 上一个if判断是否有值,这里判断不是null null会去数据库查询 能ull和empty不一样
return null;//外层在失败封装
}
/**
* 缓存没命中 查询数据库重建缓存
*
*/
//3.不存在在则去mysql数据进行查找
// 3.1获取互斥锁
boolean istrylock = trylock( id.toString());
// 3.2判断使否互获取成功
Shop shop = null;
try {
if (!istrylock){
// 3.3失败则当前该线程休眠并且重试(避免多线程同时操作 导致数据不一致)
// 休眠
Thread.sleep(50);
// 重试递归
return queryWithMutex(id);
}
// 3.4获取成功再次查询redis做了递归数据以及重建是否
String s2 = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);
if (s2!= null){
Shop shop1 = JSONUtil.toBean(s2, Shop.class);
return shop1;
}
// 3.5 则数据重建redis
shop = getBaseMapper().selectById(id);
Thread.sleep(200);//模拟数据模拟重建的线程
//4.开始重建 mysql中未查到
if (Objects.isNull(shop)){
//4.若在数据库中没有发现则返回null
// 给redis 写"null" 防止有人恶意读写数据库(io)崩坏 并且数据自动销毁 避免管理员数据库更新数据后 而用户看到的还是空值
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,"",5,TimeUnit.MINUTES);//什么都不设是null,''叫做empty
return null;
}
//5.若查到到对应数据即可返回 并且更新缓存区
String shopjson = JSONUtil.toJsonStr(shop);
//6. 设置缓存数据生命周期 自动删除 确保数据无论是否手动更新成功都清楚 让用户下次访问时写入缓存
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX+id,shopjson,30, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);//异常抛出
}finally {
// 7. 释放互斥锁
unlock(id.toString());//无论异常如何都要把这个锁删了
}
//返回数据
return shop;
}
}
把缓存区对应数据清空,使用jmeter5秒进行2000次请求测试
输出日志,只进行了一次数据库的读写 说明防止击穿成功并且线程线程安全
和Mybatis中的逻辑删除类似,给常用的·热点数据设置一个过期状态码,设置过期但是并不会在缓存中消失
实现逻辑
逻辑过期时间:
封装带有逻辑过期时间的实体
/*
逻辑删除的封装类型
*/
@Data
public class RedisData {
private LocalDateTime expireTime;//逻辑过期时间
private Object data;//存入redis的数据
}
这里分别把缓存击穿的俩种解决方式定义为俩种方法:
service实现类
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 根据id查询商铺信息
* 添加redis缓存和数据库同步功能
* 1.更新数据时候 先更新数据库
* 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
* 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
*
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 用互斥锁的方式演示缓存击穿
// Shop shop = queryWithMutex(id);
// 用逻辑删除 方式演示缓存击穿
Shop shop = queryWithLogicExpire(id);
if (shop == null) {
return Result.fail("没有店铺信息");
}
return Result.ok(shop);
}
/**
*
* 用逻辑删除格式重建缓存数据
* @param id
* @param expireSeconds
*/
public void saveShop2Redis(Long id,Long expireSeconds) {
// 1.查询店铺数据
Shop shop = getById(id);
try {
Thread.sleep(200L);//模拟重建缓存的延时
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2. 封装逻辑过期时间
RedisData redisdata = new RedisData();
redisdata.setData(shop);
redisdata.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//设置传入过期时间
// 3.写入缓存
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,JSONUtil.toJsonStr(redisdata));
}
/**
* 更新数据库
* 添加redis缓存和数据库同步功能
* 1.更新数据时候 先更新数据库
* 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
* 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
*
* @return
*/
@Override
@Transactional//定位事务
public Result update(Shop shop) {
// 1.更新数据库
updateById(shop);
// 2.删除缓存
if (shop.getId()==null) {
return Result.fail("店铺id 不能为空");
}
stringRedisTemplate.delete(HmConstants.SHOP_PREX+shop.getId());
//3.将数据库操作和缓存操作设置为一个事务一个操作出错发生回滚 确保同步
return Result.ok();
}
/**
* 尝试获取锁(模仿互斥锁)
*/
private boolean trylock(String id){
// 使用redis充当锁 setnx操作
String lockey = "lock:shop:";
// 当前线程如果没有锁则设置成功 返回true 有锁则返回false 设置到期时间防止锁在某个线程很久不结束
Boolean rflog = stringRedisTemplate.opsForValue().setIfAbsent(lockey+id, "huchilock", 5, TimeUnit.SECONDS);
return BooleanUtil.isTrue(rflog);//直接返回布尔值 实际调用拆箱(booleanvalue)方法 如果为null 会报指针异常
}
//释放锁 执行完防止缓存击穿过后把锁的数据删除
private void unlock(String id){
stringRedisTemplate.delete("lock:shop:"+id);
}
// 缓存穿透缓存击穿实现
//
public Shop queryWithMutex(Long id){
//1.从redis缓存中查寻缓存
/**
* 因为这里使用的是forvalue操作对象
* 获取出来的对象是java字符串
* 所以我们需要反序列化成java对象
*/
String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
// 判断是否纯在 并且判断是否为空值缓存穿透策略
if (StrUtil.isNotBlank(s)){//该方法空值也会执行空字符和null返回false
//2.如果存在返回数据 先解析(反序列化)为java对象
Shop shop = JSONUtil.toBean(s, Shop.class);
return shop;
}
// 判断是否是否为我们设置的防穿透值
if (s!=null) {//这里不能使用.equals("") 这样很会报错null指针异常
//或者s!=null 上一个if判断是否有值,这里不能是null null会去数据库查询 能ull和empty不一样
return null;//外层在做循环
}
/**
* 缓存没命中 查询数据库重建缓存
*
*/
//3.不存在在则去mysql数据进行查找
// 3.1获取互斥锁
boolean istrylock = trylock( id.toString());
// 3.2判断使否互获取成功
Shop shop = null;
try {
if (!istrylock){
// 3.3失败则当前该线程休眠并且重试(避免多线程同时操作 导致数据不一致)
// 休眠
Thread.sleep(50);
// 重试递归
return queryWithMutex(id);
}
// 3.4获取成功再次查询redis做了递归数据以及重建是否
String s2 = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);
if (s2!= null){
Shop shop1 = JSONUtil.toBean(s2, Shop.class);
return shop1;
}
// 3.5 则数据重建redis
shop = getBaseMapper().selectById(id);
Thread.sleep(200);//模拟数据模拟重建的线程
//4.开始重建 mysql中未查到
if (Objects.isNull(shop)){
//4.若在数据库中没有发现则返回null
// 给redis 写"null" 防止有人恶意读写数据库(io)崩坏 并且数据自动销毁 避免管理员数据库更新数据后 而用户看到的还是空值
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,"",5,TimeUnit.MINUTES);//什么都不设是null,''叫做empty
return null;
}
//5.查到到对应数据即可返回 并且更新缓存区
String shopjson = JSONUtil.toJsonStr(shop);
//6. 设置缓存数据生命周期 自动删除 确保数据无论是否手动更新成功都清楚 让用户下次访问时写入缓存
stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX+id,shopjson,30, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);//异常抛出
}finally {
// 7. 释放互斥锁
unlock(id.toString());//无论异常如何都要把这个锁删了
}
//返回数据
return shop;
}
/**
* 热点数据
* 用逻辑过期解决缓存击穿问题
*/
//线程执行器 cache_rebuild_executor
private static final ExecutorService cache_rebuild_executor = Executors.newFixedThreadPool(10);//新建10个线程
public Shop queryWithLogicExpire(Long id) {
// 1 查询缓存
String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
// 2. 判断是否命中
if (StrUtil.isBlank(s)) {
// 3. 没命中返回null
return null;
}
// 4. 命中,需要把json转化为对象
RedisData redisData = JSONUtil.toBean(s, RedisData.class);
// 取出数据 json转化二次封装的object 是josnObjecr
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
// 取出过期时间
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否逻辑过期 过期时间是否当前时间之后
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1 未过期返回店铺信息
return shop;
}
// 5.2 过期 需要缓存重建
// 6. 缓存重建
// 6.1 获取互斥锁
boolean istrylock = trylock(id.toString());
// 6.2 判断获取成功与否
// 6.3 获取锁成功开启独立线程 实现缓存重建: 查询,封装,写入缓存 定义在方法saveShop2Redis
if (istrylock) {
cache_rebuild_executor.submit(
() -> {
try {
this.saveShop2Redis(id, 30L);//写入缓存并且添加逻辑过期时间
} finally {
//释放锁 避免思索
this.unlock(id.toString());
}
}
);//创建的10个线程添加任务
}
// 6.4 失败直接返回过期商铺信息
return shop;
}
}
提前在数据库中,更改数据,并逻辑过期时间到期
用1s 100个线程进行获取请求接口
只要有俩个线程拿到了锁进行了 sql语句的查询 重建过期数据的缓存说明线程安全 没有堵塞
未来解决以上常见问题 所以进行封装一个简单的工具类,以求能够达到:
方法 1,3 解决常见数据的缓存穿透,2,4解决热点高并发数据的缓存击穿,使用springdata序列化好的Stringredistemplate为基础,不适用默认的redistemplate(读写数据需要自己序列化).
封装的解决常见问题的缓存工具类
CacheClient
//封装一个redis缓存工具类
@Slf4j//日志
@Component//注入ioc管理 也避免注解失效
public class CacheClient {
// 不能使用注解注入静态变量
/**
*/
@Autowired
private StringRedisTemplate stringRedisTemplate ;
/**
* 放缓存穿透
* @param key
* @param value
* @param Time
* @param unit
*/
public void set(String key, Object value, Long Time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),Time,unit);
}
/**
*设置封装逻辑删除的对象
* @param key
* @param value
* @param time 逻辑删除时间有效期
* @param unit 时间单位
*/
public void setWithLogicExpire(String key, Object value,Long time,TimeUnit unit){
//new 逻辑过期对象
RedisData data = new RedisData();
//封装属性和逻辑过期时间
data.setData(value);
data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 把包含逻辑过期的数据写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data));
}
/**
* 用于查询缓存并且防止缓存穿透
*
* 根据缓存前缀和id 获取缓存
* @param keyprefix 缓存前缀 因为不知道查询哪个功能相关的缓存
* @param id
* @param bean
* @param 自定义返回类型
* @param
* @return
* // 获取返回值 因为不知道返回值是什么 所以使用泛型指定
* //定义返回值泛型·和id 泛型
*/
public <R,ID> R querywithPassThrough(String keyprefix, ID id, Class<R> bean, Function<ID,R> dbFallback, Long time, TimeUnit unit) {
String key = keyprefix+id;
// 1从redis 查询商品缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (Strings.isNotBlank(json)) {
// 3.存在返回直接
return JSONUtil.toBean(json,bean); //字节码反射
}
// 4.既然不纯在值 则判断是否为""空值
if (json!= null){
// 再次强调 null!="" 但是equals 和大多方法会将俩种数据混成一个
// 为防止穿透设置的空值""
return null;
}
// 5.为null 说明未命中缓存 从数据库中查询 作为工具类并不知道
//5.1 作为工具类并不知道 查询那一张表 故此定义函数让调用者自己传入方法
R r=dbFallback.apply(id);
// 6.对数据库的查询结果经行判断
if (r== null){
// 6.2数据库没有该数据,做空值处理 防止穿透
//6.3Long time,TimeUnit unit加入时间 单位参数 让用户可以定义空白值存在时间 平且可以为空即默认时间
stringRedisTemplate.opsForValue().set(key,"",time,unit);
return null;//返回错误
}
//7. 数据库中存在该数据 写入很缓存
set(key,r,time,unit);
return r;
}
//线程池执行器 cache_rebuild_executor
private final ExecutorService cache_rebuild_executor = Executors.newFixedThreadPool(10);//新建10个线程
/**
* 查询热点数据
* 用逻辑删除的方式
* 防止缓存击穿
* @param cacheprefix 逻辑删除缓存的前缀
* @param id
* @param ResultType
* @param dbFallback 数据库操作函数
* @param time
* @param unit
* @param 返回类型
* @param id
* @return
*/
public <R,ID> R queryWithLogicExpire(String cacheprefix,ID id,Class<R> ResultType,Function<ID,R> dbFallback, Long time, TimeUnit unit) {
String key=cacheprefix+id;
// 1 查询缓存
String s = stringRedisTemplate.opsForValue().get(key);//店铺对应的key
// 2. 判断
if (StrUtil.isBlank(s)) {
// 3. 没命中返回null
return null;
}
// 4. 命中,需要把json转化为逻辑删除的封装对象
RedisData redisData = JSONUtil.toBean(s, RedisData.class);
// 取出数据 反序列化成传入字节码类型
R r = JSONUtil.toBean((JSONObject) redisData.getData(),ResultType);
// 取出过期时间
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否逻辑过期 过期时间是否当前时间之后
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1 未过期返回店铺信息
return r;
}
// 5.2 过期 需要缓存重建
// 6. 缓存重建
// 6.1 获取互斥锁
boolean istrylock = trylock(id.toString());
// 6.2 判断获取成功与否
// 6.3 获取锁成功开启独立线程 实现缓存重建: 查询,封装,写入缓存
if (istrylock) {
cache_rebuild_executor.submit(
() -> {
try {
// 查询,封装,写入缓存 对任意数据表操作,所以将操作过程作为参数传入
R DB_r = dbFallback.apply(id);
setWithLogicExpire(key,DB_r,time,unit);
} finally {
//释放锁 避免思索
unlock(id.toString());
}
}
);//创建的10个线程添加任务
}
// 6.4 获取锁失败直接返回过期商铺信息
return r;
}
/**
* 尝试获取互斥锁
* @param id
* @return
*/
private boolean trylock(String id){
// 使用redis充当锁
String lockey = "lock:shop:";//锁前缀
// 当前线程如果没有锁则设置成功 返回true 有锁则返回false 设置到期时间防止锁在某个线程很久不结束
// setnx操作
Boolean rflog = stringRedisTemplate.opsForValue().setIfAbsent(lockey+id, "huchilock", 5, TimeUnit.SECONDS);
return BooleanUtil.isTrue(rflog);//直接返回布尔值 实际调用拆箱(booleanvalue)方法 如果为null 会报指针异常
}
//释放锁 执行完防止缓存击穿过后把锁的数据删除
private void unlock(String id){
stringRedisTemplate.delete("lock:shop:"+id);
}
}
其中的Redis Data 其实只是封装了时间是数据的类
Redis Data代码:
/*
逻辑删除的封装类型
*/
@Data
public class RedisData {
private LocalDateTime expireTime;//逻辑过期时间
private Object data;//存入redis的数据
}
演示jumeter 用上工具类的缓存击穿
1秒,1000个线程 才读写了 一次数据库 说明缓存没有被击穿 并没有线程安全问题
自定义工具类调用:
Shop shop = cacheClient.querywithPassThrough(HmConstants.SHOP_PREX, id, Shop.class, this::getById, 10L, TimeUnit.SECONDS);