缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高
之前没有使用缓存是的模型
当我们查询商家信息的时候,直接从mysql中获取的。现在我们将原来的项目改造。改造地方在ShopController,我们按照流程图去做,添加redis缓存,业务都是在service中实现的。
# 具体实现流程
1 redis中查询商户缓存
2 判断是否存在
3 存在直接返回
4 不存在根据id去数据库查询
5 数据库也不存在,返回错误
6 存在则写入redis中
7 返回
<dependency>
? ?<groupId>com.alibaba</groupId>
? ?<artifactId>fastjson</artifactId>
? ?<version>1.2.47</version>
</dependency>
/**
? ?* 根据id查询商铺信息
? ? * @param id 商铺id
? ? * @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
? ?return shopService.queryById(id);
? ?// ? ? ? return Result.ok(shopService.getById(id));
}
@Resource
private RedisTemplate<String,Object> redisTemplate;
?
@Override
public Result queryById(Long id) {
? ?//1 redis中查询商户缓存
? ?String shopJson = (String)redisTemplate.opsForValue().get("cache.shop:" + id);
? ?//2 判断是否存在
? ?if(StrUtil.isNotBlank(shopJson)){
? ? ? ?//3存在直接返回
? ? ? ?Shop shop = JSONUtil.toBean(shopJson, Shop.class);
? ? ? ?return Result.ok(shop);
? }
? ?//4 不存在根据id去数据库查询
? ?Shop shop = this.getById(id);
? ?//5 数据库也不存在,返回错误
? ?if(shop==null){
? ? ? ?return Result.fail("店铺不存在");
? }
? ?//6 存在则写入redis中
? ?redisTemplate.opsForValue().set("cache.shop:" + id,JSONUtil.toJsonStr(shop));
? ?//7 返回
? ?return Result.ok(shop);
}
@Autowired
private RedisTemplate<String, Object> redisTemplate;
?
@Override
public Result queryTypeList() {
?
? ?List<Object> list = redisTemplate.opsForList().range("cache.list",0,-1);
? ?if(list!=null && list.size()!=0){
? ? ? ?return Result.ok(list);
? }
? ?List<ShopType> sort = this.query().orderByAsc("sort").list();
? ?if(sort==null||sort.size()==0){
? ? ? ?return Result.fail("列表不存在");
? }
? ?sort.forEach(s->redisTemplate.opsForList().rightPush("cache.list",s));
? ?return Result.ok(sort);
}
1 思路
查询数据的时候,如果缓存未命中,则查询数据库,将数据写入缓存设置超时时间
修改数据时,先修改数据库,在删除缓存。
延时双删策略
2 代码实现
修改更新方法,添加超时时间
@Override
public Result queryById(Long id) {
//1 redis中查询商户缓存
String shopJson = (String)redisTemplate.opsForValue().get("cache.shop:" + id);
//2 判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4 不存在根据id去数据库查询
Shop shop = this.getById(id);
//5 数据库也不存在,返回错误
if(shop==null){
return Result.fail("店铺不存在");
}
//6 存在则写入redis中
redisTemplate.opsForValue().set("cache.shop:" + id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
//7 返回
return Result.ok(shop);
}
修改ShopController
?
@PutMapping
? ?public Result updateShop(@RequestBody Shop shop) {
? ? ? ?// 写入数据库
?
? ? ? ?//shopService.updateById(shop);
? ? ? ?//return Result.ok();
? ? ? ?return ?shopService.update(shop);
? }
修改service代码 延时双删策略
?@Override
? ?public Result update(Shop shop) {
?
? ? ? ?Long id = shop.getId();
? ? ? ?if(id==null){
? ? ? ? ? ?return Result.fail("店铺id不存在");
? ? ? }
? ? ? ? // 删除缓存
? ? ? ?redisTemplate.delete("cache.shop:" + id);
? ? ? ?// 更新数据库
? ? ? ?updateById(shop);
? ? ? ?Thread.sleep(800);
? ? ? ?// 删除缓存
? ? ? ?redisTemplate.delete("cache.shop:" + id);
? ? ? ?return Result.ok();
? }
3 修改完代码以后,将所有的缓存删除,执行查询操作,多了超时
4 用postman执行修改方法: localhost:8081/shop
{
"area":"大关",
"sold":3035,
"address":"金华路锦昌文华苑29号",
"name":"102茶餐厅",
"x":120.149192,
"y":30.316078,
"typeId":1,
"id":1
}
执行完成以后,数据库的数据发生改变,查看redis的数据已经删除了。
客户端请求的数据,在数据库和redis中都不存在,这样缓存永远都不会生效,请求最终都到了数据库上。
解决方案:
当在数据库查询的结果也不存在的时候,可以返回null值给redis,并且设置TTL
布隆过滤器
布隆过滤器是一种数据结构,底层是位数组,通过将集合中的元素多次hash得到的结果保存到布隆过滤器中。主要作用就是可以快速判断一个元素是否在集合里面,但是因为算法的原因,也有一定概率的错误。
开发的时候我们一般选择空值值方式。
代码方式实现
根据id查询的时候,如果信息不存在,则要将空值写入redis,并设置空值过期时间
@Override
public Result queryById(Long id) {
//1 redis中查询商户缓存
String shopJson = (String)redisTemplate.opsForValue().get("cache.shop:" + id);
//2 判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if(shopJson!=null){
return Result.fail("店铺不存在");
}
//4 不存在根据id去数据库查询
Shop shop = this.getById(id);
//5 数据库也不存在,返回错误
if(shop==null){
// 空值写入redis中
redisTemplate.opsForValue().set("cache.shop:" + id,"",3, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//6 存在则写入redis中
redisTemplate.opsForValue().set("cache.shop:" + id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
//7 返回
return Result.ok(shop);
}
也叫热点key问题,一个被高并发访问且业务复杂的key突然失效了,无数的请求瞬间给数据库带来的巨大冲击
解决方案: 互斥锁 逻辑过期
互斥锁思路:
查询缓存的时候,未命中需要获取锁 代码ShopServiceImpl
@Override
public Result queryById(Long id) {
//1 redis中查询商户缓存
String shopJson = (String)redisTemplate.opsForValue().get("cache.shop:" + id);
//2 判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if(shopJson!=null){
return Result.fail("店铺不存在");
}
Shop shop = null;
String lockKey = "lock.id:" + id;
try {
//代码到这里说明没有命中缓存,那么就可以获取锁了
boolean isLock = tryLock(lockKey);
// 如果没有拿到锁,则等待一会,递归执行代码
if(!isLock){
Thread.sleep(100);
queryById(id);
}
//获取锁成功
//4 不存在根据id去数据库查询
shop = this.getById(id);
//5 数据库也不存在,返回错误
if(shop==null){
// 空值写入redis中
redisTemplate.opsForValue().set("cache.shop:" + id,"",3, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//6 存在则写入redis中
redisTemplate.opsForValue().set("cache.shop:" + id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock(lockKey);
}
//7 返回
return Result.ok(shop);
}
// 获取锁
private boolean tryLock(String key){
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key){
redisTemplate.delete(key);
}
同一时间段内,大量的缓存key失效或者redis宕机,到时大量的请求到达数据库,带来巨大的压力。
解决方案
给key设置随机的TTL
集群方案防止宕机不可用