目录
一、缓存
1、缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落
盘工作。
哪些数据适合放入缓存?
*即时性、数据一致性要求不高的
*访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率
来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
2、redis
redis是什么?
Redis = Remote Dictionary Server,即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
链接: 官网下载地址
3、整合redis作为缓存
在product.xml下引入redis模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
本机下载redis,并启动
yml下配置redis:
修改三级目录查询方法,实现缓存查询:
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson(){
//给缓存中放json字符串,拿出的json字符串,还用逆转为能用的对象类型:{序列化和反序列化}
/**
* 1.空结果缓存: 解决缓存穿透
* 2.设置过期时间(加随机值): 解决缓存雪崩
* 3.加锁: 解决缓存击穿
*/
//1. 加入缓存逻辑
//JSON跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON)){
//2.缓存中没有,则查询数据库
Map<String,List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
return catalogJsonFromDb;
}
//转为我们的指定对象
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
//从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
/**
*
* 1.优化:将数据库查询变为一次
*/
//只要是同一把锁,就能锁住需要这个锁的所有线程
//1.synchronized(this):Springboot所有的组件在容器中都是单例的
//TODO 本地锁:synchronized,JUC(lock),在分布式情况下,想要锁住所有,必须使用分布式锁
synchronized (this){
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(!StringUtils.isEmpty(catalogJSON)){
//如果缓存不为null直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 1.查出所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//2.封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1. 每一个的一级分类,查到这个一级分类下的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
//2.封装上面结果
List<Catelog2Vo> catelog2Vos = null;
if(categoryEntities != null){
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1.找到当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
if(level3Catelog != null){
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
//封装指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
//3.查到的数据放入缓存,将对象转换为JSON放在缓存中
String s = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catalogJSON",s);
return parent_cid;
}
}
再次执行查询操作,可以用redis管理工具(RedisDesktopManager)看到如下缓存:
二、缓存失效问题
1、缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数
据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次
请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是
漏洞。
解决:
缓存空结果、并且设置短的过期时间。
2、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失
效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决:
原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的
重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,
是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所
有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
解决:
加锁!!!
三、分布式锁
1、分布式锁与本地锁
2、分布式锁基本原理
演进一阶段:
演进二阶段:
演进三阶段:
演进四阶段:
演进最后阶段:
3、使用RedisTemplate 操作分布式锁
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去 redis 占坑
String uuid = UUID.randomUUID().toString();
Boolean lock =
redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
//2、设置过期时间,必须和加锁是同步的,原子的
//redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList("lock"), uuid);
}
//获取值对比+对比成功删除=原子操作 lua 脚本解锁
// String lockValue = redisTemplate.opsForValue().get("lock");
// if(uuid.equals(lockValue)){
// //删除我自己的锁
// redisTemplate.delete("lock");//删除锁
// }
return dataFromDb;
}else {
//加锁失败...重试。synchronized ()
//休眠 100ms 重试
System.out.println("获取分布式锁失败...等待重试");
try{
Thread.sleep(200);
}catch (Exception e){
}
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
4、使用Redisson完成分布式锁
1 Redisson简介
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。充分
的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者
提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工
具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式
系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间
的协作。
官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
2 配置
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
Config config = new Config();
//redis://127.0.0.1:7181
//可以用"rediss://"来启用 SSL 连接
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
RedissonClient redisson = Redisson.create(config);
3 使用分布式锁
RLock lock = redisson.getLock("anyLock");// 最常见的使用方法
lock.lock();
// 加锁以后 10 秒钟自动解锁// 无需调用 unlock 方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
使用其他锁,参考官网文档
四、缓存数据一致性
1、双写模式
2、失效模式
3、解决方案
• 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
• 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
• 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
• 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
• 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
• 总结:
• 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
• 我们不应该过度设计,增加系统的复杂性
• 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
改进方案1:
分布式读写锁。读数据等待写数据整个操作完成
改进方案2:
五、Spring Cache
1、简介
SpringCache是Spring提供的一个缓存框架,在Spring3.1版本开始支持将缓存添加到现有的spring应用程序中,在4.1开始,缓存已支持JSR-107注释和更多自定义的选项。
Spring 从 3.1 开始定义了 org.springframework.cache.Cache和org.springframework.cache.CacheManager 接口来统一不同的缓存技术;
并支持使用 JCache(JSR-107)注解简化我们开发;
Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合;
Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已
经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓
存结果后返回给用户。下次调用直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
2、基础概念
3、注解
4、表达式语法
5、实际运用
1.导入spring-cache依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.spirng-cache自定义配置文件
public class MyCacheConfig {
/**
* 配置文件中的东西没用用上
* 1.原来和配置文件绑定的配置类是这样子的
* @ConfigurationProperties(prefix = "Spring.cache")
* public class CacheProperties
*
* 2、要让他生效
* @EnableConfigurationProperties(CacheProperties.class)
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//配置文件都生效
if(redisProperties.getTimeToLive() != null){
config = config.entryTtl(redisProperties.getTimeToLive());
}
if(redisProperties.getKeyPrefix() != null){
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if(!redisProperties.isCacheNullValues()){
config = config.disableCachingNullValues();
}
if(!redisProperties.isUseKeyPrefix()){
config = config.disableKeyPrefix();
}
return config;
}
}
3.application.properties相关常用配置
spring.cache.type=redis
spring.cache.redis.time-to-live=3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
4.方法注解具体使用
1)在需要用到的缓存方法上加上注解
2)更新写方法,可以用其他注解
3)效果测试:
访问商城网站
redis中可以看到,有我们两个方法保存的缓存结果
当我们在后端系统修改菜单一些信息
可以看到redis中缓存的数据都没了,需要重新查询,则我们简单的cache运用结束