缓存就是数据交换的缓冲区,读写性能较高
举例:web应用开发离不开缓存。用户向浏览器发起请求,首先就可以建立浏览器缓存(常见页面静态资源缓存到本地),大大降低网络的延迟,提高页面响应速度,浏览器未命中的数据就会去到tomcat也就是我们编写的java应用(添加应用层缓存:简单来说创建一个map,我们从数据库查到数据放到map里面,以后再来的时候直接从map读给你,这样以来减少数据库的查询,效率提升)一般使用redis,缓存未命中的话请求依然还会落到数据库(数据库添加缓存:索引mysql数据库是一个聚簇索引它会给id创建索引,这些索引数据就可以缓存起来,这样以来当我们根据索引查询数据内存快速检索,得到结果,不用每次读写磁盘,效率大大提升)当然最终数据去查找还是要落到磁盘,还有做些复杂排序,表关联,CPU做运算,数据库还会访问到CPU和磁盘,这个时候自然就会用到CPU多级缓存以及磁盘读写缓存
商品查询缓存
之前直接从数据库查询响应时间长,现在进行redis优化,提高读写效率,降低响应时间
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
//return Result.ok(shopService.getById(id));
return shopService.queryById(id);
}
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis中查询商铺缓存
String key=CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4.不存在,则去数据库查询
Shop shop = getById(id);
// 5.如果找不到,则返回错误信息
if (shop==null){
return Result.fail("店铺不存在");
}
// 6.如果找到,则把商铺信息存放到redis当中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
// 7.返回
return Result.ok(shop);
}
}
明显加快了许多,从1.71s变为15ms
商铺类型缓存
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
// List<ShopType> typeList = typeService
// .query().orderByAsc("sort").list();
// return Result.ok(typeList);
return typeService.queryShopList();
}
}
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryShopList() {
// 1. 从redis中查询商铺类型列表
List<String> shopTypes = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, 9);
// 2. 命中,返回商铺类型信息
List<ShopType> shopTypesByRedis = new ArrayList<>();
if (shopTypes.size() != 0) {
for (String shopType : shopTypes) {
ShopType type = JSONUtil.toBean(shopType, ShopType.class);
shopTypesByRedis.add(type);
}
return Result.ok(shopTypesByRedis);
}
// 3. 未命中,从数据库中查询商铺类型,并根据sort排序
List<ShopType> shopTypesByMysql = query().orderByAsc("sort").list();
// 4. 将商铺类型存入到redis中
for (ShopType shopType : shopTypesByMysql) {
String s = JSONUtil.toJsonStr(shopType);
stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY,s);
}
// 5. 返回商铺类型信息
return Result.ok(shopTypesByMysql);
}
}
时间27ms就完成了
缓存更新策略
先操作缓存和先操作数据库(胜出)两者异常情况 (其中操作数据库出现异常需满足以下条件:线程1开始执行时刚好出现缓存失效,然后步骤1和步骤4这段微妙的时间内线程2刚好发生更新数据库和删除缓存)线程安全考虑
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Transactional
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if (id==null){
return Result.fail("商铺Id不能为空");
}
// 先操作数据库
updateById(shop);
// 再删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
}
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
// shopService.updateById(shop);
return shopService.update(shop);
}
}
由于修改信息得在管理端执行,为了方便,我们在postman测试
数据库查验由原来的102变为103 且redis已经删除缓存了
此时重新刷一下网站http://127.0.0.1:8080/shop-detail.html?id=1
由原来102茶餐厅变为103餐厅,redis更新缓存
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库都不存在,这样缓存永远不会生效,这些请求都会打到数据库带来巨大压力 (优先选择方案一)
@Override
public Result queryById(Long id) {
// 1.从redis中查询商铺缓存
String key=CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.1 判断是否存在(数据真实存在)
if (StrUtil.isNotBlank(shopJson)) {
// 3.1 存在,直接返回对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 3.2 存在(数据为空字符串),返回错误信息
// 等价于"".equals(shopJson)
if (shopJson!=null){
return Result.fail("店铺信息不存在");
}
// 4.不存在,则去数据库查询
Shop shop = getById(id);
// 5.如果找不到,则返回错误信息
if (shop==null){
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
// 返回错误信息
return Result.fail("店铺不存在");
}
// 6.如果找到,则把商铺信息存放到redis当中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
// 7.返回
return Result.ok(shop);
}
第一次请求不存在的id时,它会查询数据库,如果找不到会把空值存放到redis缓存当中,下次继续访问该id时,就不会查询数据库,直接从缓存那里返回错误信息
缓存穿透主动的解决方案
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
缓存击穿
缓存击穿也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
案例:基于互斥锁方式解决缓存击穿问题(一致性)
需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
// 尝试获取锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 直接返回会做拆箱,有可能返回空指针,所以使用工具类
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
// 缓存击穿
public Shop queryWithMutex(Long id){
// 1.从redis中查询商铺缓存
String key=CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.1 判断是否存在(数据真实存在)
if (StrUtil.isNotBlank(shopJson)) {
// 3.1 存在,直接返回对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
// return Result.ok(shop);
return shop;
}
// 3.2 存在(数据为空字符串),返回错误信息
// 等价于"".equals(shopJson)
if (shopJson!=null){
// return Result.fail("店铺信息不存在");
return null;
}
// 4.实现缓存重建
// 4.1 获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 判断是否获取成功
if (!isLock){
// 4.3 失败,则休眠并重试
Thread.sleep(20);
queryWithMutex(id);
}
// 4.4 成功,根据id数据库查询
shop = getById(id);
// 模拟重建的延时
Thread.sleep(200);
// 5.如果找不到,则返回错误信息
if (shop==null){
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
// 返回错误信息
// return Result.fail("店铺不存在");
return null;
}
// 6.如果找到,则把商铺信息存放到redis当中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放互斥锁
unlock(lockKey);
}
// 8.返回
// return Result.ok(shop);
return shop;
}
注意:获取锁成功应该再次检测redis缓存是否存在,做doublecheck,如果存在则无需重建缓存(这里没有实现)
ctrl+alt+6出现异常选项
业务逻辑层Service实现
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queryWithPassThrough(id);
// 缓存击穿
Shop shop = queryWithMutex(id);
if (shop==null){
Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
案例:基于逻辑过期方式解决缓存击穿问题(可用性)
需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
第一种方案我们可以使用Shop类继承RedisData类(已经添加过期时间),但是我们要修改原代码(不推荐)
第二种方案我们在RedisData类添加Object data属性
package com.hmdp.utils;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
public void saveShop2Redis(Long id,Long expireSeconds){
//1.查询店铺数据
Shop shop = getById(id);
Thread.sleep(200);// 延迟时间,为了验证出些问题
//2.封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入redis (key永久有效,过期由我们控制,没有设置TTL
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
测试一下redis缓存
package com.hmdp;
import com.hmdp.service.impl.ShopServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class HmDianPingApplicationTests {
@Resource
private ShopServiceImpl shopService;
@Test
void testSaveShop(){
shopService.saveShop2Redis(1L,10L);
}
}
理论上讲永久存在,逻辑上已经过期了
获取到的锁可能刚好是上一个线程刚重建好缓存释放的
// 线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
// 缓存击穿2
public Shop queryWithLogicalExpire(Long id){
// 1.从redis中查询商铺缓存
String key=CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.1 判断是否存在(数据真实存在)
if (StrUtil.isBlank(shopJson)) {
// 3.1 存在,直接返回null
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// Object data = redisData.getData();
// 返回的是Object类型但我们需要强转 以便转换成需要Shop类型
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
// 5.1 未过期,直接返回店铺信息(旧的)
return shop;
}
// 5.2 已过期,需要缓存重建
// 6.缓存重建
// 6.1 获取互斥锁
String lockKey= LOCK_SHOP_KEY+id;
// 6.2 判断是否获取锁成功
boolean isLock = tryLock(lockKey);
if (isLock){
// 这里需要再次检查redis缓存是否过期(获取到的锁刚好是上一个线程缓存重建释放的)
if (expireTime.isAfter(LocalDateTime.now())){
// 5.1 未过期,直接返回店铺信息(旧的)
return shop;
}
// 6.3 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()-> {
try {
// 重建缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4 返回过期的商铺信息
return shop;
}
100个线程1秒执行,发现一开始查询到的数据还是缓存中的103餐厅,中间某一刻过了200ms后面查询到的才是数据库中的102餐厅 (有一段时间不一致,后面就一致了)发现只进行一次缓存重建