redis缓存

缓存就是数据交换的缓冲区,读写性能较高

举例: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时,就不会查询数据库,直接从缓存那里返回错误信息

缓存穿透主动的解决方案

  1. 增强id的复杂度,避免被猜测id规律
  2. 做好数据的基础格式校验
  3. 加强用户权限校验
  4. 做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存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餐厅 (有一段时间不一致,后面就一致了)发现只进行一次缓存重建

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值