Redis 实现分布式缓存

缓存

1. 什么是缓存?

缓存就是数据交换的缓冲区,用于临时存储数据(使用频繁的数据)。当用户请求数据时,首先在缓存中寻找,如果找到了则直接返回。如果找不到,则去数据库中查找

缓存的本质就是用空间换时间,牺牲数据的实时性,从而减轻数据库压力,尽可能提高吞吐量,有效提升响应速度

2. 缓存的分类

缓存的应用范围十分广泛,常见的有文件缓存、浏览器缓存、数据库缓存等等,但我们今天着重关注的是 WEB 应用服务领域,根据缓存与应用的耦合度,可以分为本地缓存和分布式缓存:

  • 本地缓存

    指在应用中的缓存组件,最大的优点是应用和缓存是在同一个进程内部,请求缓存速度快;同时,它的缺点也是因为缓存跟应用程序耦合,多个应用程序无法直接共享缓存,各应用或集群的各节点都需要维护自己的单独缓存

  • 分布式缓存

    指的是与应用分离的缓存组件或服务,最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接共享缓存

3. 缓存的特点

缓存也是一个数据模型对象,那么必然有它的一些特征:

  • 命中率

    命中率 = 返回正确结果数 / 请求缓存次数,命中率是衡量缓存有效性的重要指标,命中率越高,表明缓存的使用率越高

  • 最大元素

    缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值,将会触发缓存清空策略。根据不同的场景合理设置最大元素值,可以在一定程度上提高缓存的命中率,从而更有效的利用缓存

4. 缓存清空策略

缓存的存储空间有限制,当缓存空间被用满时,就需要缓存清空策略来处理,常见的一般策略有:

  • 先进先出策略:先进入缓存的数据,在缓存空间不足时会被优先被清理掉,在数据实效性要求较高的场景下,可选择该策略

  • 最少使用策略:无论是否过期,根据元素被使用的次数判断,清除使用次数较少的元素。最少使用策略主要比较元素的命中次数,在保证高频数据有效性场景下,可选择该策略

  • 最近最少使用策略:无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素。策略算法主要比较元素最近一次被使用的时间,适用于热点数据场景

此外,还有一些简单策略,比如:

  • 根据过期时间判断,清理过期时间最长的元素
  • 根据过期时间判断,清理最近要过期的元素
  • 随机清理
  • 根据关键字(或元素内容)清理等等

5. 分布式缓存应用场景

  • 冷热分离:将缓存作为热数据层,将数据库作为冷数据层,在数据写入数据库的同时向缓存写入一份,当请求到来时先判断缓存是否存在该数据,存在则直接返回,否则查询数据库,并将数据库的数据回写到缓存
  • 热数据存储:热数据存储和冷热存储分离的区别在于:在热数据存储场景下,数据并不需要在冷存储中存储。在一些高并发应用场景下,如果数据结构简单,则可直接将数据存储在分布式缓存中,以增加系统的并发效率
  • 计数器:可以通过分布式缓存如 Reids 实时统计一些业务指标,例如用户每天登录次数、核心 API 的访问次数、文章的点赞次数和阅读次数等
  • 分布式锁:利用 Redis 的 setnx 方法实现分布式锁
  • 分布式 Session:在分布式环境下将用户的 Session 信息存储在分布式缓存中,以实现分布式环境下的用户身份验证
  • 全局 ID:利用 Redis 的 incrby 的原子性操作特性,生成全局唯一的 ID

Redis 实现分布式缓存

可以利用 Mybatis 自带的本地缓存,结合 Redis 实现分布式缓存,主要思路是将 Mybatis 二级缓存的存放地点从本地改为配置了 Redis 的远程服务器

1. 开启 mybatis 二级缓存

创建一个 SpringBoot 工程,整合 MyBatis 和 Redis,在 Mapper 文件中加入 <cache/> 标签开启二级缓存

<cache/> 标签默认采用 PrepetualCache,该类是 Cache 接口的实现类,维护一个 Map 来保存数据

2. 自定义 cache 实现

我们要作改造,就要自定义一个实现类并替换 <cache type="xxxx.RedisCache">

实现自定义 RedisCache

public class RedisCache implements Cache {

  	// 当前放入缓存的 mapper 的 namespace,也是缓存的唯一标识
    private final String id;

    public RedisCache(String id) {
        System.out.println("id:" + id);
        this.id = id;
    }


    /**
     * 返回 cache 的唯一标识
     */
    @Override
    public String getId() {
        return this.id;
    }

    /**
     * 缓存放入值
     */
    @Override
    public void putObject(Object key, Object value) {
        System.out.println("放入缓存");
        // 通过工具类获取 redisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();
        // 使用 redishash 类型作为缓存存储模型
        redisTemplate.opsForHash().put(id.toString(), key.toString(), value);
    }

    /**
     * 获取缓存中的值
     */
    @Override
    public Object getObject(Object key) {
        System.out.println("获得缓存");
        // 通过工具类获取 redisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();
        // 根据 key 从 redis 的 hash 类型中获取数据
        return redisTemplate.opsForHash().get(id.toString(), key.toString());

    }

    /**
     * 根据指定的 key 删除缓存
     * 该方法为 mybatis 保留方法,默认没有实现
     */
    @Override
    public Object removeObject(Object key) {
        System.out.println("根据指定的 key 删除缓存");
        return null;
    }

    @Override
    public void clear() {
        System.out.println("清空缓存");
        // 通过工具类获取 redisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();
        // 清空 namespace
        redisTemplate.delete(id.toString());
    }

    /**
     * 计算缓存数量
     */
    @Override
    public int getSize() {
        RedisTemplate redisTemplate = getRedisTemplate();
        return redisTemplate.opsForHash().size(id.toString()).intValue();
    }

    /**
     * 获取 redisTemplate
     */
    private RedisTemplate getRedisTemplate() {
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

3. 处理表连接查询时的问题

到此为止,使用 Redis 实现分布式缓存的目标就完成了,但还有一点要注意的是,涉及到多表查询时,结果会包含另一个表的对象信息。在这里对缓存进行存储的时候,使用的是 mapper 的 那么 namespace 作为 唯一标识,这样一来当调用 clear 方法时只会清理本身 namespace 的缓存,被包含的另一个表的对象信息不会被清理。如果此时表信息发生改变,将导致数据不一致

解决办法是每个 namespace 都使用同一个缓存,如下所示,表示当前 dao 和 UserDao 共享同一个缓存

<!-- 共享其他 namespace 的缓存 -->
<cache-ref namespace="com.zk.UserDao"/>

Redis 缓存优化

1. 键值优化

key 的长度不能太长,尽可能简短,使用 MD5 进行优化处理:

  • 一切文件字符串经过 md5 处理后,都会生成 32 位 16 进制字符串
  • 不同内容文件经过 md5 加密,结果一定不一致
  • 相同内容文件经过多次 md5 处理,结果始终一致

使用 SpringBoot 提供的 MD5 加密的工具类即可实现

String s = DigestUtils.md5DigestAsHex(key.getBytes());

2. 缓存预热

缓存预热指在用户请求数据前先将数据加载到缓存系统中,用户查询事先被预热的缓存数据,以提高系统查询效率。缓存预热一般有系统启动加载、定时加载等方式

3. 缓存更新

缓存更新指在数据发生变化后及时将变化后的数据更新到缓存中,常见的缓存更新策略有如下四种:

  • 定时更新:定时将底层数据库内的数据更新到缓存中,该方法比较简单,适合需要缓存的数据量不是很大的应用场景
  • 过期更新:定时将缓存中过期的数据更新为最新数据并更新缓存的过期时间
  • 写请求更新:在用户有写请求时,先写数据库同时更新缓存,这适用于用户对缓存数据和数据库的数据有实时强一致性要求的情况
  • 读请求更新:在用户有读请求时,先判断该请求数据的缓存是否存在或过期,如果不存在或已过期,则进行底层数据库查询并将查询结果更新到缓存中,同时将查询结果返回给用户

4. 缓存穿透(击穿)

客户端查询了一个数据库中没有的数据,导致缓存在这种情况下无法利用(数据库都没有则缓存更不可能有了),此情况下可绕过缓存直接攻击数据库

解决方法有如下几种:

  • 指将所有可能存在的数据都映射到布隆过滤器,发起请求前先经过布隆过滤器的拦截,一定不存在的数据会被这个布隆过滤器拦截,从而避免访问数据库
  • 缓存空结果,就是对查询不存在的数据也记录在缓存中,这样就可以有效的减少查询数据库的次数

5. 缓存雪崩

在系统运行的某一时刻,缓存全部失效,恰好这一时刻涌来大量客户端请求,导致数据库阻塞或挂起。导致缓存失效的原因有很多,常见的是缓存到了失效时间(所有的缓存设置了同样的过期时间),而没有作合适的处理

解决方法有如下几种:

  • 设置缓存永久存储(不推荐)
  • 为每一个缓存数据都增加过期标记来记录缓存数据是否失效,如果缓存标记失效,则更新缓存数据
  • 针对不同业务数据设置不同的超时时间,防止集体失效

6. 缓存降级

缓存降级指由于访问量剧增导致服务出现问题时,优先保障核心业务的运行,减少或关闭非核心业务对资源的使用,常见的降级策略如下:

  • 写降级:在写请求增大时,可以只进行缓存的更新,然后将数据异步更新到数据库中,保证最终一致性即可
  • 读降级:在数据库服务负载过高或数据库系统发生故障时,可以只对缓存进行读取并将结果返回给用户,只是访问的数据相对有延迟

7. Redis 内存淘汰策略

Redis 内存淘汰策略是指当内存使用达到设置的最大内存限制时,用于选择删除哪些键值对来释放内存的策略

  • noeviction:不淘汰,内存满时报错
  • volatile-lru:从设置过期时间的键中淘汰最近最少使用的
  • volatile-ttl:从设置过期时间的键中淘汰剩余存活时间最短的
  • volatile-random:从设置过期时间的键中随机淘汰
  • allkeys-lru:从所有键中淘汰最近最少使用的
  • allkeys - random:从所有键中随机淘汰
posted @   低吟不作语  阅读(743)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示