一、缓存
1、缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作
哪些数据适合放入缓存?
1)即时性、数据一致性要求不高的
2)访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
伪代码:
data = cache.load(id);//从缓存加载数据
If(data == null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//保存到 cache 中
}
return data;
注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题。
二、整合redis作为缓存
1、引入redis-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置文件中配置redis
spring:
redis:
host: 192.168.56.10
port: 6379
3、使用redisTemplate操作redis
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("hello","world_"+ UUID.randomUUID().toString());
String hello = ops.get("hello");
System.out.println(hello);
}
三、缓存失效问题
1、缓存穿透
1、缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
2、在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞
缓存穿透示意图:
缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中,虽然对redis缓存服务器不会造成影响,但是数据库的负载就会增大,使得缓存的作用降低。
3、 解决方案:
缓存空结果,并且设置过期时间
缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)
缓存空对象会有两个问题:
第一,value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象
2、缓存雪崩(大面积同时失效)
1、缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩
2、解决方案:
原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
1、 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
2、 这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
3、解决方案:
加锁
4、使用synchronized加锁,在分布式的情况下存在的问题
假如有8台机器,使用synchronized加锁,只能锁住一台机器,有8台机器,第一台机器把10000次请求锁住了,只放一个进来,第二台机器也是同样,把10000次的请求锁住,只放一个请求进来,其他机器都一样。 就会有8次请求放进来查询数据库查询相同的数据,那么有几台机器就会有几个线程进来。还是没有解决以上的问题,因为synchronized只能锁住各自服务。
在分布式的情况下,必须使用分布式锁来解决
四、分布式锁
1、分布式锁的概念
分布式锁是指分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。
本地锁在分布式系统中存在的问题:
当系统部署在多个机器中时
请求方式:就是从单进程多线程的情况下变为多进程的环境
如果还是使用本地锁,只能锁住当前进程,所以需要分布式锁
2、分布式锁的实现方式
分布式锁就是所有的线程到同一个地方占锁[set一把锁],如果可以set进去,说明占锁成功(获取锁成功),则可以进行下一步的业务逻辑处理。执行完业务后再删除锁.
3、分布式锁的几个阶段演变过程
1、分布式锁 阶段一
2、分布式锁 - 阶段二
阶段二存在的问题,在占锁之后和设置锁的过期时间之前这个时间段出现系统异常(例如:突然断电…)等意外情况时,导致设置锁的过期时间这段代码没有执行,那么这个锁就永远存在,还是没有解决上面的死锁的问题。所有设置过期时间和占位必须是原子性的
代码:
设置占锁和设置锁的过期时间可以使用一条命令搞定
set lock[锁名称] 111 EX 300【锁过期时间】 NX
以上问题解决的思路是,设置锁的过期时间必须和加锁是同步的,原子的
/**
* 在占坑的同时,设置过期时间,是为了防止死锁的问题,添加过期时间和加锁必须是原子性的
*/
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
3、分布式锁 阶段三
阶段三存在以下两个问题
1)业务超时怎么处理?
2)自己执行的业务时间大于锁的过期时间,在执行业务的时候,自己的锁过期后自动删除了,等业务执行完成后,有可能把别人正在持有的锁删除了。如何保证删除的不是别人的锁?
获取值对比 + 对比成功删除 = 原子操作
4、分布式锁 阶段四
5、分布式锁 阶段五
使用脚本来解决 “获取值对比 + 对比成功删除 = 原子操作”
分布式锁的核心:加锁【占锁 + 设置过期时间】必须是原子性
删除锁【获取值对比 + 对比成功删除】必须是原子性
核心代码:
/**
* 添加分布式锁
* @return
*/
public Map<String, List<Catelog2Vo>> getCategorysFromDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
/**
* 在占坑的同时,设置过期时间,是为了防止死锁的问题,添加过期时间和加锁必须是原子性的
*/
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
log.info("获取分布式锁成功.....");
// 加锁成功......执行业务
/**
* 如果在此处设置锁的过期时间,当程序在占坑的时候出现异常,或者突然断电的极端情况,
* 程序无法执行到此处,则该锁永远不会过期,就会出现死锁的情况
* **********所有添加过期时间和加锁的过程必须是原子性的*********
*/
// redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> cateoryData;
try {
cateoryData = getCateoryDataFromDb();
/**
* 删除自己的锁
* 获取值对比+ 对比成功删除=原子操作
*/
/*String lockValue = redisTemplate.opsForValue().get("uuid");
if(uuid.equals(lockValue)){
redisTemplate.delete("lock");
}*/
} finally {
/**
* redis的Lua脚本原子解锁
*/
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<>(script, Long.class), Arrays.asList("lock"), uuid);
}
return cateoryData;
}else{
// 加锁失败....... 自旋重试
log.info("获取分布式锁失败,等待重试...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCategorysFromDbWithRedisLock();
}
}
五、Redisson完成分布式锁
1、简介
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
2、使用方法
1)配置redisson
@Configuration
public class MyRedissonConfig {
/**
* 所有对redisson的使用都是通过redissonClient对象
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.18.38:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
2)使用分布式锁
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 使用Redisson实现分布式锁
* 缓存中的数据如何和数据库保持一致
* @return
*/
public Map<String, List<Catelog2Vo>> getCategorysFromDbWithRedissonLock() {
// 1、占分布式锁,去redis占坑,锁的名字必须区分
RLock lock = redissonClient.getLock("catalogJson-lock");
lock.lock();
log.info("获取分布式锁成功.....");
Map<String, List<Catelog2Vo>> cateoryData;
try {
// 2、执行业务代码
cateoryData = getCateoryDataFromDb();
} finally {
// 3、释放锁
lock.unlock();
}
return cateoryData;
}
--------------------------------------------------------------------------------------------
private Map<String, List<Catelog2Vo>> getCateoryDataFromDb() {
// 查询redis缓存,如果缓存中存在,则直接返回,否则,执行下面查询数据库的操作
String categoryJson = redisTemplate.opsForValue().get("categoryJson");
if(!StringUtils.isEmpty(categoryJson)){
Map<String, List<Catelog2Vo>> result = JSON.parseObject(categoryJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
log.info("查询了数据库.....");
//查询出一级分类
List<CategoryEntity> level1Categorys = getParent_cid(categoryEntityList,0L);
// 封装数据
Map<String, List<Catelog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> catLevel2 = getParent_cid(categoryEntityList,v.getCatId());
List<Catelog2Vo> catelog2 = null;
if (catLevel2 != null && !catLevel2.isEmpty()) {
catelog2 = catLevel2.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
// 2、查询当前二级分类的三级分类封装成vo
List<CategoryEntity> catLevel3 = getParent_cid(categoryEntityList,l2.getCatId());
if(catLevel3 != null && !catLevel3.isEmpty()){
List<Catelog2Vo.Catelog3Vo> catelog3Vos = catLevel3.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catelog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2;
}));
String s = JSONObject.toJSONString(collect);
// 将查询到的数据放入缓存中 设置过期时间为1天
redisTemplate.opsForValue().set("categoryJson",s,1, TimeUnit.DAYS);
return collect;
}
六、SpringCache实现缓存
1、整合springCache简化缓存开发
1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
2)配置
(1)自动配置了哪些?
CacheAutoConfiguration会导入RedisCacheConfiguration,自动配置好了缓存管理器RedisCacheManager
**配置使用redis作为缓存,在application.properties中配置**
spring.cache.type=redis
3)开启缓存功能,在主启动类中使用@EnableCaching注解开启使用缓存
@EnableCaching
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@MapperScan("com.atguigu.gulimall.product.dao")
@SpringBootApplication
@EnableDiscoveryClient
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
2、几个重要的注解
1、@Cacheable
@Cacheable注解:使用在当前方法的结果需要缓存,如果缓存中有,方法就不用调用。如果缓存中没有,会调用方法,最后将方法的结果放入缓存中。【这个注解一般用在查询方法上】
/**
* 每个需要缓存的数据我们都来指定要放到那个名字的缓存【缓存分区(按照业务类型分)】
* @return
*/
@Cacheable({"category"})
@Override
public List<CategoryEntity> getLevel1Categorys() {
long startTime = System.currentTimeMillis();
List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
System.out.println("查询一级菜单消耗的时间:{}" + (System.currentTimeMillis() - startTime));
return entities;
}
添加了@Cacheable注解后,调用接口,第一次会查询数据库,将结果存入到缓存中,第二次…就直接在缓存中取数据啦。@Cacheable默认行为:(1)如果缓存中有,方法不用调用(2)key默认自动生成:缓存的名字::SimpleKey[] (自动生成的key值)(3)缓存的value的值,默认使用jdk序列化机制,将序列化后的数据存入到redis (4) 默认ttl时间(过期时间):-1(永不过期)
2、@CachePut
加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其它地方使用。它**「通常用在新增方法上」。
**
3、@CacheEvict
使用了CacheEvict注解的方法,会清空指定缓存。「一般用在更新或者删除的方法上」。
4、@Caching
组合以上多个操作
5、@CacheConfig
在类级别共享缓存的相同配置