赞
踩
缓存就是数据交换的缓冲区,用于临时存储数据(使用频繁的数据)。当用户请求数据时,首先在缓存中寻找,如果找到了则直接返回。如果找不到,则去数据库中查找
缓存的本质就是用空间换时间,牺牲数据的实时性,从而减轻数据库压力,尽可能提高吞吐量,有效提升响应速度
缓存的应用范围十分广泛,常见的有文件缓存、浏览器缓存、数据库缓存等等,但我们今天着重关注的是 WEB 应用服务领域,根据缓存与应用的耦合度,可以分为本地缓存和分布式缓存:
本地缓存
指在应用中的缓存组件,最大的优点是应用和缓存是在同一个进程内部,请求缓存速度快;同时,它的缺点也是因为缓存跟应用程序耦合,多个应用程序无法直接共享缓存,各应用或集群的各节点都需要维护自己的单独缓存
分布式缓存
指的是与应用分离的缓存组件或服务,最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接共享缓存
缓存也是一个数据模型对象,那么必然有它的一些特征:
命中率
命中率 = 返回正确结果数 / 请求缓存次数,命中率是衡量缓存有效性的重要指标,命中率越高,表明缓存的使用率越高
最大元素
缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值,将会触发缓存清空策略。根据不同的场景合理设置最大元素值,可以在一定程度上提高缓存的命中率,从而更有效的利用缓存
缓存的存储空间有限制,当缓存空间被用满时,就需要缓存清空策略来处理,常见的一般策略有:
先进先出策略:先进入缓存的数据,在缓存空间不足时会被优先被清理掉,在数据实效性要求较高的场景下,可选择该策略
最少使用策略:无论是否过期,根据元素被使用的次数判断,清除使用次数较少的元素。最少使用策略主要比较元素的命中次数,在保证高频数据有效性场景下,可选择该策略
最近最少使用策略:无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素。策略算法主要比较元素最近一次被使用的时间,适用于热点数据场景
此外,还有一些简单策略,比如:
可以利用 Mybatis 自带的本地缓存,结合 Redis 实现分布式缓存,主要思路是将 Mybatis 二级缓存的存放地点从本地改为配置了 Redis 的远程服务器
创建一个 SpringBoot 工程,整合 MyBatis 和 Redis,在 Mapper 文件中加入 <cache/>
标签开启二级缓存
<cache/>
标签默认采用 PrepetualCache,该类是 Cache 接口的实现类,维护一个 Map 来保存数据
我们要作改造,就要自定义一个实现类并替换 <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; } }
到此为止,使用 Redis 实现分布式缓存的目标就完成了,但还有一点要注意的是,涉及到多表查询时,结果会包含另一个表的对象信息。在这里对缓存进行存储的时候,使用的是 mapper 的 那么 namespace 作为 唯一标识,这样一来当调用 clear 方法时只会清理本身 namespace 的缓存,被包含的另一个表的对象信息不会被清理。如果此时表信息发生改变,将导致数据不一致
解决办法是每个 namespace 都使用同一个缓存,如下所示,表示当前 dao 和 UserDao 共享同一个缓存
<!-- 共享其他 namespace 的缓存 -->
<cache-ref namespace="com.zk.UserDao"/>
key 的长度不能太长,尽可能简短,使用 MD5 进行优化处理:
使用 SpringBoot 提供的 MD5 加密的工具类即可实现
String s = DigestUtils.md5DigestAsHex(key.getBytes());
客户端查询了一个数据库中没有的数据,导致缓存在这种情况下无法利用(数据库都没有则缓存更不可能有了),此情况下可绕过缓存直接攻击数据库
对于这种恶意访问,一种思路是先做校验,对恶意数据直接过滤掉,不要发送至数据库层;第二种思路是缓存空结果,就是对查询不存在的数据也记录在缓存中,这样就可以有效的减少查询数据库的次数
在系统运行的某一时刻,缓存全部失效,恰好这一时刻涌来大量客户端请求,导致数据库阻塞或挂起。导致缓存失效的原因有很多,常见的是缓存到了失效时间(所有的缓存设置了同样的过期时间),而没有作合适的处理
要解决这个问题,一种方式是设置缓存永久存储(不推荐),另一种方式是针对不同业务数据设置不同的超时时间,防止集体失效
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。