0
点赞
收藏
分享

微信扫一扫

音视频学习笔记——H264编码

扒皮狼 2024-03-08 阅读 17

1、什么是缓存?

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码

1.1、为什么要使用缓存?

添加缓存后,重复的数据可以直接从缓存中获取,一定程度上降低服务器的压力
但缓存带来的问题:

缓存作用使用缓存成本
降低后端负载数据一致性成本
提高读写效率,提升响应能力维护成本

1.2、如何使用缓存?

实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用
浏览器缓存:主要是存在于浏览器端的缓存
应用层缓存: 可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存
数据库缓存: 在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中
CPU缓存: 当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存
image-20220523212915666.png

2、添加商铺缓存

  • 缓存作用模型

1653322097736.png

  • 核心代码
public Shop queryById(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    if (shopJson != null) {
        // 如果缓存中有,直接返回
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        return shop;
    }
    // 如果缓存中没有,从数据库中查询
    Shop shop = this.getById(id);
    // 如果数据库中没有,返回null
    if (shop == null) {
        return null;
    }
    // 将查询到的数据写入缓存
    stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop));
    return shop;
}

3、缓存更新策略

缓存更新是redis为了节约内存而设计的一种机制,当往redis中插入过多数据,会导致缓存数据过多造成宕机的可能,因此redis会对一些数据进行淘汰。

  • 内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
  • 超时剔除:当给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
  • 主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
/内存淘汰超时剔除主动更新
说明不用我们手动维护,当缓存中的数据过多时,利用redis内存淘汰机制,自动剔除部分数据,下次查询会自动更新当数据过期时,会被自动剔除。下次查询时进行自动更新当数据库中的数据进行更改时,手动地对缓存中的数据进行更新
一致性一般
成本

3.1、缓存与数据库不一致问题

当数据发生更改时,数据库更改了而缓存没有同步更新,或者缓存更新了而数据库没有同步更新,都会造成数据不一致的问题。

3.2、不一致问题解决方案

不一致问题有三种解决方案:

  • Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
  • Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
  • Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

分析:
方案一可以实现,但具有一定的维护成本
方案二仍存在较大的一致性问题
方案三会存在数据丢失问题,如缓存数据还没来得及写入数据库,redis就崩溃了,那么这部分数据没有被持久化而丢失
综上,最终选择方案一作为数据不一致问题的解决方案

方案一如何处理实现,仍有三个问题需要考虑:

  • 是删除缓存还是更新缓存?
  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库让缓存失效,再次查询时更新缓存,实现懒加载效果
  • 如何保证缓存操作与数据库操作同时成功或失败?
    • 单体系统:使用事务机制保证操作的原子性
    • 分布式系统:使用TCC等分布式解决方案来保证操作的原子性
  • 先更新数据库再删除缓存,还是先删除缓存再更新数据库?
  • 先删除缓存再操作数据库
  • 先更新数据库再删除缓存

因为缓存操作是快操作、数据库操作是慢操作。第一种方案在并发情况下更加容易出现数据不一致的问题。
1653323595206.png

4、实现商铺数据缓存与数据库的同步修改

核心思路:

  • 查询商铺数据时,如果缓存没有数据,则查询数据库,同步更新到缓存并设置过期时间;
  • 更新数据库数据时,直接删除缓存数据

核心代码

/**
 * 案例:给查询商铺的缓存添加超时剔除和主动更新的策略
 * (先操作数据库后删除缓存)
 * @param id 商铺id
 * @return
 */
@Override
public Shop queryById(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    if (shopJson != null) {
        // 如果缓存中有,直接返回
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        return shop;
    }
    // 如果缓存中没有,从数据库中查询
    Shop shop = this.getById(id);
    // 如果数据库中没有,返回null
    if (shop == null) {
        return null;
    }
    // 将查询到的数据写入缓存
    stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop),
            RedisConstants.SHOP_KEY_TTL, TimeUnit.MINUTES);
    return shop;
}
@Override
@Transactional // 保证数据库操作和缓存操作的原子性
public boolean updateByShopId(Shop shop) {
    if (shop.getId() == null) {
        return false;
    }
    // 更新数据库
    updateById(shop);
    // 更新缓存
    stringRedisTemplate.delete(RedisConstants.SHOP_KEY + shop.getId());
    return true;
}

5、缓存穿透

5.1、什么是缓存穿透?

缓存穿透指的是:当请求到达redis和数据库均未命中,且客户端发来大量这种恶意请求,造成数据库服务宕机的问题成为缓存穿透。

5.2、缓存穿透解决方案

常见的缓存穿透解决方案有两种:

  • 缓存空对象
  • 布隆过滤器
优点缺点
缓存空对象实现简单造成内存存储大量垃圾数据;存在短期的不一致
布隆过滤器内存占用较少,没有多余key实现复杂存在误判可能

缓存空对象思路分析:当redis和数据库均未命中,则向redis中对应的key缓存一条空对象,当再次查询同一key时,直接返回空对象即可,减少数据库的压力。
布隆过滤器: 布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,但因为其实通过hash的思想进行判断,会存在误判的可能。

1653326156516.png

5.3、缓存空对象编码实现

  • 核心代码
public Shop queryById(Long id) {
// 从缓存中查询
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
if (StrUtil.isNotBlank(shopJson)) {
    // 如果缓存中有,直接返回
    Shop shop = JSON.parseObject(shopJson, Shop.class);
    return shop;
}
// 判断redis是否缓存空值 shopJson != null,说明其是""空字符串
if (shopJson !=null){
    return null;
}
// 如果缓存中未命中,从数据库中查询
Shop shop = this.getById(id);
// 如果数据库中没有,返回null
if (shop == null) {
    // 数据库也未命中,缓存null值到redis
    stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, SHOP_NULL_VALUE,
                                          RedisConstants.SHOP_NULL_VALUE_TTL, TimeUnit.MINUTES);
    return null;
}
// 将查询到的数据写入缓存
stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop),
                                      RedisConstants.SHOP_KEY_TTL, TimeUnit.MINUTES);
return shop;
}

5.4、补充说明

缓存穿透的解决方案当然不止前面说的这两种,还可以考虑:

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

6、缓存雪崩

6.1、什么是缓存雪崩?

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

6.2、解决方案

  • 在设置热点key时,设置不同的TTL,避免热点key同时失效
  • 添加redis集群,提供高可用服务
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

7、缓存击穿

7.1、什么是缓存击穿?

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

逻辑分析:在线程1重建缓存过程的同时,其他大量线程也都查询缓存,然后走查询数据库重建缓存这样的过程,对数据库服务造成巨大压力。
1653328022622.png
解决方案:

  • 互斥锁

只有获取到锁资源的线程可以重建缓存,其他线程则需要一直等待重试,知道缓存中有数据或拿到锁资源。
1653328288627.png

  • 逻辑过期

在设置热点key时不设置TTL,而是在数据本身中设置一个逻辑过期时间,这样在redis中这个数据基本可以认为永不失效。通过代码来判断数据是否失效,当线程1查询数据失效时,尝试获取锁资源,开启独立线程进行缓存重建,而自己则返回旧数据。
同时,其他大量线程查询数据失效时,也尝试获取锁资源。如果失败,则不等待,直接返回旧数据即可!
1653328663897.png

  • 方案优缺点比对
优点缺点
互斥锁实现简单没有额外内存消耗保证数据一致性性能较差,大量线程需要等待存在死锁风险
逻辑过期线程无需等待,服务可用性好不保证一致性;实现复杂;存在额外内存消耗(存储逻辑过期时间)

7.2、利用互斥锁解决缓存击穿

  • 核心思路

image.png

  • 核心代码
private Shop queryWithMutexLock(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    if (StrUtil.isNotBlank(shopJson)) {
        // 如果缓存中有,直接返回
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        return shop;
    }
    // 判断redis是否缓存空值 shopJson != null,说明其是""空字符串
    if (shopJson != null) {
        return null;
    }
    try {
        // 获取互斥锁
        if (!getMutexLock(id)) {
            // 未获取到锁,等待一段时间后重试
            Thread.sleep(50);
            queryWithMutexLock(id);
        }

        // Double Check
        shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            // 如果缓存中有,直接返回
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            return shop;
        }
        // 判断redis是否缓存空值 shopJson != null,说明其是""空字符串
        if (shopJson != null) {
            return null;
        }

        // 从数据库中查询,进行缓存重建
        Shop shop = this.getById(id);
        // 数据库也未命中,缓存null值到redis
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, SHOP_NULL_VALUE,
                    RedisConstants.SHOP_NULL_VALUE_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 将查询到的数据写入缓存
        stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop),
                RedisConstants.SHOP_KEY_TTL, TimeUnit.MINUTES);
        return shop;
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 释放互斥锁
        releaseMutexLock(id);
    }
}
/**
 * 获取互斥锁
 * @param id
 * @return
 */
private boolean getMutexLock(Long id) {
    String key = "lock:shop" + id;
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 不要直接返回包装类,存在空指针异常问题,需要使用工具类BooleanUtil.isTrue()方法进行校验
    return BooleanUtil.isTrue(flag);
}
/**
 * 释放互斥锁
 * @param id
 */
private void releaseMutexLock(Long id) {
    String key = "lock:shop" + id;
    stringRedisTemplate.delete(key);
}

7.3、利用逻辑过期解决缓存击穿

  • 核心思路

1653360308731.png

  • 核心代码
/**
 * 查询商铺信息(缓存击穿-逻辑过期)
 *
 * @param id
 * @return
 */
private Shop queryWithLogicExpireTime(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    // 判断是否过期
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    if (StrUtil.isNotBlank(shopJson)) {
        // 未过期,返回商铺
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            return shop;
        }
    }

    try {
        // 尝试获取互斥锁
        if (!getMutexLock(id)) {
            // 未获取到锁,返回旧商品信息
            return shop;
        }

        // Double Check
        shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            // 检查缓存是否存在,且是否过期
            shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
            if (StrUtil.isNotBlank(shopJson)) {
                // 判断是否过期
                redisData = JSONUtil.toBean(shopJson, RedisData.class);
                shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
                // 未过期,返回商铺
                if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
                    return shop;
                }
            }
        }

        // 从数据库中查询,进行缓存重建
        shop = this.getById(id);
        // 数据库也未命中,返回null
        if (shop == null) {
            return null;
        }
        // 将查询到的数据写入缓存,设置逻辑过期
        RedisData newRedisData = new RedisData();
        newRedisData.setData(shop);
        newRedisData.setExpireTime(LocalDateTime.now().plusMinutes(RedisConstants.SHOP_KEY_TTL));
        stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(newRedisData));
        return shop;
    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        // 释放互斥锁
        releaseMutexLock(id);
    }
}

8、简单封装缓存工具类

@Component
public class CacheUtil {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }
}
举报

相关推荐

0 条评论