为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据持久化
工作。
哪些数据适合放入缓存?
-
即时性、数据一致性要求不高的
即时性:物流状态信息,可能五分钟或者五十分钟看一次,频率看的高,但更新的速度是很慢的,我们对即时性的要求也不高。一致性:数据库与缓存不一致,例如分类,修改之后我们可能并不需要立马保持一致,不会产生太大的影响。
-
访问量大且更新频率不高的数据(读多,写少)
商品一旦录入之后,我们很少去做一些修改,但是我们经常会查询商品的信息,我们就可以把它放到缓存中。
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的
使用redis缓存
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: 192.168.56.10
package com.atlinxi.gulimall.product.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atlinxi.gulimall.product.service.CategoryBrandRelationService;
import com.atlinxi.gulimall.product.vo.Catelog2Vo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;
import com.atlinxi.gulimall.product.dao.CategoryDao;
import com.atlinxi.gulimall.product.entity.CategoryEntity;
import com.atlinxi.gulimall.product.service.CategoryService;
import org.springframework.util.StringUtils;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 之前是使用@Autowired注入
// 现在继承了 mybatisplus的ServiceImpl,泛型是CategoryDao,
// 我们可以直接使用baseMapper,它代表的也就是CategoryDao
// @Autowired
// private CategoryDao categoryDao;
/**
* 级联更新所有相关数据
* @param category
*/
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
if (!StringUtils.isEmpty(category.getName())){
// 同步更新其他关联表中的数据
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
// todo 更新其他关联表
}
}
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 给缓存中放json字符串,拿出的json字符串,还要逆转为java对象 【序列化】
// 1. 加入缓存逻辑,缓存中传的所有对象都是json字符串
// json跨语言,跨平台兼容,例如php也可以从redis中获取使用
// 如果是java序列化的话,那就不行了
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)){
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
String s = JSON.toJSONString(catalogJsonFromDb);
stringRedisTemplate.opsForValue().set("catalogJSON",s);
return catalogJsonFromDb;
}
// 转为我们指定的类型
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
// 从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
// 性能优化,将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
// 1. 查出所有一级分类数据
List<CategoryEntity> level1Categorys = getParentCid(selectList,0L);
// 2. 封装数据
// 这儿的key和value都是level1Categorys
Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(key -> key.getCatId().toString(), value -> {
// 1. 每一个的一级分类,查询这个一级分类的二级分类
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
.eq("parent_cid", value.getCatId()));
// 2. 封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(level2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(value.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());
// 1. 找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
.eq("parent_cid", level2.getCatId()));
if (level3Catelog != null){
// 2. 封装成指定格式
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(level3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(level2.getCatId().toString(),level3.getCatId().toString(),level3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parentCid;
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
@Override
public List<CategoryEntity> listWithTree() {
// 1. 查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
List<CategoryEntity> entities2 = entities.stream()
.sorted((categoryEntity1,categoryEntity2)->(categoryEntity1.getCatLevel() - categoryEntity2.getCatLevel()))
.collect(Collectors.toList());
// 2. 组装成父子的树形结构
// 2.1 找到所有的一级分类
List<CategoryEntity> level1Menu = entities.stream()
// 过滤
// 这里用的是 == ,0应该是int,categoryEntity.getParentCid() 是Long,==就是true,equals就是false,因为equals底层会先比较类型
// getChildrens 中 categoryEntity.getParentCid().equals(root.getCatId()) 这俩都是Long类型,==就是false,equals就是true
// equals是true很好理解,因为类型一样,值一样,==是false是因为,不是在常量池中创建的对象就是在堆中创建的对象,所以地址值肯定是不一样的
.filter(categoryEntity -> categoryEntity.getParentCid() == 0
// 上面的过滤会得到一个list,
// menu只是一个变量名,指list返回的每一个元素
).map(menu -> {
menu.setChildren(getChildrens(menu, entities2));
return menu;
}).sorted((menu1, menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
// 把结果收集成一个list
.collect(Collectors.toList());
return level1Menu;
}
/**
* 递归查找所有菜单的子菜单
*
* @param root 当前分类
* @param all 所有分类
* @return
*/
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream()
.filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
.map(categoryEntity -> {categoryEntity.setChildren(getChildrens(categoryEntity,all)); return categoryEntity;})
.sorted((menu1,menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
.collect(Collectors.toList());
return children;
}
@Override
public void removeMenuByIds(List<Long> asList) {
//todo 1.检查当前删除的菜单,是否被别的地方引用
// 逻辑删除
baseMapper.deleteBatchIds(asList);
}
/**
* 根据三级分类查找其路径
* @return
*/
@Override
public Long[] findCatelogPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
Collections.reverse(parentPath);
return paths.toArray(new Long[parentPath.size()]);
}
private List<Long> findParentPath(Long catelogId,List<Long> paths){
paths.add(catelogId);
// mabatis-plus return this.getBaseMapper().selectById(id);
CategoryEntity byId = this.getById(catelogId);
if (byId.getParentCid()!=0){
findParentPath(byId.getParentCid(),paths);
}
return paths;
}
private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList,Long parentCid){
return selectList.stream().filter(item-> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
}
}
本地锁
为了解决缓存穿透的问题,我们需要加锁
synchronized (this)
本地锁使用这个,this代表实例,springboot中的对象是单例的,所以在单节点的情况下是没问题的,但是在分布式下,spring的单例针对的是一个服务,分布式是多服务,那么就是有几个服务就会去查几次数据库,这就失去了锁的意义。
锁的时序问题
我们需要把需要锁的一整套流程都加入锁中,否则就会出现时序问题。
分布式锁
分布式锁演进-基本原理
其实就是redis加锁和解锁都需要原子性,然后value不能是固定的(随机的大字符串)。
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1. 分布式占锁,去redis占坑
String uuid = UUID.randomUUID().toString();
// redis nx 命令
// 业务执行完了,删锁的时候机器断电了,其他服务器想占锁就占不到了,就造成死锁
// 设置过期时间来解决这个问题,而且必须是同步的,原子的
// 删锁的时候,如果v是固定的,则有可能误删
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300, TimeUnit.SECONDS );
if (lock){
Map<String, List<Catelog2Vo>> dataFromDb;
try{
// 加锁成功,执行业务
dataFromDb = getDataFromDb();
}finally {
// 判断v是否是自己的与对比成功删除 也应该是原子性的
// 使用redis+lua脚本来实现
// 如果redis调用了get命令 等于 我们传递的值,则删除,否则返回0
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
// stringRedisTemplate.delete("lock"); // 删除锁
return dataFromDb;
}else {
// 加锁失败,重试
// 休眠100ms,重试
return getCatalogJsonFromDbWithRedisLock(); // 自旋的方式
}
redisson
Redisson是一个基于Java的分布式Java对象和服务的框架,专门设计用于在分布式环境中操作各种数据结构。
上面的代码已经基本上可以实现分布式锁的功能,但是我们每段业务代码都需要写一套,虽然我们可以写个工具类,但redisson更专业。
使用redisson完成以后所有分布式锁功能
<!-- 以后使用redisson作为所有分布式锁,分布式对象等功能框架-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
package com.atlinxi.gulimall.product.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class MyRedissonConfig {
/**
* 所有对redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException{
// 创建配置
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.56.10:6379");
return Redisson.create(config);
}
}
可重入锁(Reentrant Lock)和看门狗机制
函数a调用函数b,两个函数都需要加锁,而且两者加的是同一把锁,
a函数被调用,a函数调用b函数,因为a函数此时已经加锁了,b函数知晓a函数加锁了,b函数直接拿来用,给a返回结果,然后a释放锁。
相反的,不可重入锁
就是a拿到锁之后调用b,a的锁不给b用,就造成死锁了。
所以我们所有的锁都应该设计为可重入锁,避免涉及到死锁的问题。
@ResponseBody
@GetMapping("/hello")
public String hello(){
// 1. 获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redissonClient.getLock("mylock");
try {
// 2. 加锁
// lock.lock(); // 阻塞式等待,默认加的锁都是30s时间
// 1)锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s不用担心业务时间长,锁自动过期会删掉的问题
// 2)加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
lock.lock(10, TimeUnit.SECONDS); // 10s自动解锁,自动解锁时间一定要大于业务的执行时间
// 问题:lock.lock(10, TimeUnit.SECONDS); 在锁时间到了之后,不会自动续期。
// 1. 如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
// 2. 如果我们未指定超时时间,就使用 30 * 1000【LockWatchdogTimeout看门狗的默认时间】
// 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】
// 1/3的看门狗时间,10s,会自动续期,续成30s
// 最佳实战,
// lock.lock(10, TimeUnit.SECONDS); 省掉了整个续期操作。
// 如果设置了一个很大的时间还不能解锁,那其实说明业务逻辑可能有问题了
System.out.println("加锁成功,执行业务。。。。。。" + Thread.currentThread().getId());
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 解锁 假设解锁代码没有运行,redisson会不会出现死锁
System.out.println("释放锁:" + Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
公平锁
多个线程抢占锁,先到先得,反之,非公平锁就是释放锁之后,线程再竞争锁。
读写锁
读的时候需要等待写的锁释放,
@ResponseBody
@GetMapping("/write")
public String writeValue(){
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
String s = null;
// 1. 改数据加写锁,读数据加读锁
RLock rLock = readWriteLock.writeLock();
rLock.lock();
try {
s = UUID.randomUUID().toString();
Thread.sleep(30000);
stringRedisTemplate.opsForValue().set("write",s);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
// 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁,独享所),读锁是一个共享锁
// 写锁没释放读就必须等待
// 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
// 写 + 读:等待写锁释放
// 写 + 写:阻塞方式
// 读 + 写:有读锁,写也需要等待
// 只要有写的存在,都必须等待
@ResponseBody
@GetMapping("/read")
public String readValue(){
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
String s = null;
RLock rLock = readWriteLock.readLock();
// 加读锁
rLock.lock();
try {
s = stringRedisTemplate.opsForValue().get("write");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
信号量
/**
* 信号量
*
* 3车位,
* park 例如 value 是 3 ,每获取一个减一个,直到0,然后阻塞
*
* 等释放后,park才执行
*
* 可以应用在限流,假设一个信号量value为 10 * 1000,如果可以获取到才允许访问,否则得等释放后再访问
*/
@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
// park.acquire();//获取一个信号,获取一个值,占一个车位
boolean b = park.tryAcquire();// 上面那个如果没有车位会一直等待,而这个会返回false
return "ok";
}
@ResponseBody
@GetMapping("/go")
public String go() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
park.release();//释放一个车位
return "ok";
}
闭锁
/**
* 放假,锁门
* 1班没人了,2班没人了。。。
* 5个班全部走完,我们可以锁大门
*/
@ResponseBody
@GetMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.trySetCount(5L);
door.await(); // 等待闭锁都完成
return "放假了。。。";
}
@ResponseBody
@GetMapping("/gogogo")
public String gogogo(@PathVariable("id") Long id) throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.countDown(); // 计数减1
return id + "班的人走了";
}
缓存数据一致性
双写模式
脏数据解决方案
- 为写数据库、写缓存加锁
- 这种延迟可以通过设置过期时间,在得到脏数据后,重新读取的时候又会获得新的数据;就看业务对这种延迟的容忍性有多大
失效模式 最终解决方案(加读写锁)
- 第一个请求,改为1,删除缓存
- 第二个请求,改为2,删除缓存,假设这台服务器因为一些原因响应时间较长
- 当第1个请求缓存删完了以后,第3个请求进来读,缓存里面没有数据,然后读数据库,但是此时db-2还没改完,就读到了db-1的数据,
- 此时要更新缓存,如果更新比db2快的话,它更新的就是db-1的数据,db-2在执行完就会再次把缓存删除,
相当于没更新
- 此时要更新缓存,如果更新比db2慢的话,那db中的数据就是db2,缓存中的数据就是db1了
- 此时要更新缓存,如果更新比db2快的话,它更新的就是db-1的数据,db-2在执行完就会再次把缓存删除,
解决问题
- 加锁,加锁的问题是会影响效率
- 如果这个数据经常修改,而且实时性要求高的,就要考虑要不要加缓存了
我们系统的一致性解决方案就用失效模式
,
- 缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
- 读写数据的时候,加上分布式的读写锁。
- 经常写,经常读,会有很大的影响
- 经常读,偶尔写,一点儿影响都没有
解决方案
- 用户大概率不会同时提交或者删除订单,也不会同时修改用户的个性签名这些
- 菜单,商品介绍等基础数据,我们可以容忍大程度的缓存不一致
例如iphone的介绍,如果修改之后,过一两天更新也可能是没关系的
Canal是一个阿里的中间件,它伪装成mysql的一个从服务器,从服务器的特点就是mysql数据库里面有什么变化,它都会同步过来
好处:我们不需要再关心缓存的操作,只需要关心数据库的
缺点:又加了一个中间件,还得开发一些我们自定义的功能,如果一次开发成型,我们就不需要管这些事儿了
binlog:二进制日志,日志里有每一次mysql什么东西更新了
数据异构大概的意思就是,例如京东,给每个人首页推荐的东西不一样,就是Canal通过浏览记录,购物车等等,经过计算,为你推荐。
我们暂时使用缓存的数据,都定义在读多写少的场景,暂时不适用Canal
改造数据一致性代码
我们使用的是
- 失效模式,设置过期时间
- 数据一致性的解决方案是读写锁
我们发现,后来的每一个业务代码都要用缓存,用的都是这种编码模式
Spring Cache
Spring 从 3.1 开始定义了 org.springframework.cache.Cache
和 org.springframework.cache.CacheManager 接口来统一
不同的缓存技术;
并支持使用 JCache(JSR-107)注解简化
我们开发;
基础概念
我们应用中需要配一个或多个CacheManager(缓存管理器)
,可以管理多种缓存,EmpCache,Salary Cache
CacheManager(缓存管理器)> Cache(缓存组件)
CacheManager帮我们造出很多缓存组件,Cache负责缓存的读写
我们这儿使用的缓存管理器是RedisCacheManager,缓存组件是RedisCache
整合spring cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
application.properties
#自动配置了哪些
#CacheAutoConfiguration会导入RedisCacheConfiguration
#自动配好了缓存管理器 RedisCacheManager
spring.cache.type=redis
# redis key ttl ms为单位
spring.cache.redis.time-to-live=3600000
# 缓存的前缀,如果指定了前缀就用我们指定的前缀,如果没有,就默认使用缓存的名字作为前缀
# @Cacheable(value = {"category"},key = "#root.method.name")
# category::getLevel1Categorys
# 我们这里指定了,就是
# CACHE_getLevel1Categorys
spring.cache.redis.key-prefix=CACHE_
# 如果不使用前缀,那么注解中和配置文件中的都不会生效
# getLevel1Categorys
spring.cache.redis.use-key-prefix=true
# 是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
缓存注解
@Cacheable: Triggers cache population.
触发将数据保存到缓存的操作,代表当前方法的结果需要缓存,
缓存中有,读缓存,缓存中没有,存入缓存
@CacheEvict: Triggers cache eviction.触发将数据从缓存删除,失效模式
@CachePut: Updates the cache without interfering with the method execution.不影响方法执行更新缓存,双写模式
@Caching: Regroups multiple cache operations to be applied on a method.组合以上多个操作
@CacheConfig: Shares some common cache-related settings at class-level.在类级别共享缓存的相同配置
// 读模式下使用缓存
// key是指存到redis的key
// @Cacheable(value = {"category"},key = "'getLevel1Categorys'")
// 获取到函数名,当作key
@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
@Cacheable(value = "category",key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 性能优化,将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
// 1. 查出所有一级分类数据
List<CategoryEntity> level1Categorys = getParentCid(selectList,0L);
// 2. 封装数据
// 这儿的key和value都是level1Categorys
Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(key -> key.getCatId().toString(), value -> {
// 1. 每一个的一级分类,查询这个一级分类的二级分类
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
.eq("parent_cid", value.getCatId()));
// 2. 封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(level2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(value.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());
// 1. 找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
.eq("parent_cid", level2.getCatId()));
if (level3Catelog != null){
// 2. 封装成指定格式
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(level3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(level2.getCatId().toString(),level3.getCatId().toString(),level3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parentCid;
}
/**
* 级联更新所有相关数据
* @param category
*
* @CacheEvict 失效模式
*
* key 是spel表达式,如果是字符串需要加单引号,否则会默认为表达式
*
* 1. @Caching 同时进行多种缓存操作
* 2. @CacheEvict(value = "category",allEntries = true) 删除category分区下所有的数据
* 3. 存储同一类型的数据,可以指定成同一个分区。分区名默认就是缓存的前缀
*/
// @CacheEvict(value = "category",key = "'getLevel1Categorys'")
// @Caching(evict = {
// @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
// @CacheEvict(value = "category",key = "'getCatalogJson'")
// })
@CacheEvict(value = "category",allEntries = true)
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
if (!StringUtils.isEmpty(category.getName())){
// 同步更新其他关联表中的数据
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
// todo 更新其他关联表
}
}
默认配置
-
如果缓存中有,方法不用调用;如果缓存中没有,会调用方法,最后将方法的结果放入缓存
-
每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】
-
key默认自动生成,缓存的名字::SimpleKey []
-
value的值,默认使用jdk序列化机制,将序列化后的数据存到redis
-
ttl默认-1
默认配置原理
缓存自动配置 CacheAutoConfiguration
// 将所有属性绑定在CacheProperties
@EnableConfigurationProperties({CacheProperties.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class, CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})
public class CacheAutoConfiguration {
// 缓存自动配置会导入一些缓存的配置类
static class CacheConfigurationImportSelector implements ImportSelector {
CacheConfigurationImportSelector() {
}
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for(int i = 0; i < types.length; ++i) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
缓存配置类 CacheConfigurations
final class CacheConfigurations {
static {
Map<CacheType, String> mappings = new EnumMap(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class.getName());
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class.getName());
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class.getName());
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class.getName());
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class.getName());
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class.getName());
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class.getName());
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class.getName());
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class.getName());
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class.getName());
MAPPINGS = Collections.unmodifiableMap(mappings);
}
CacheAutoConfiguration帮我们导入了 RedisCacheConfiguration
package org.springframework.boot.autoconfigure.cache;
class RedisCacheConfiguration {
// 注入了缓存管理器
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
// 决定缓存用哪个配置
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
// 按照我们配置文件中配置的缓存名字
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
}
if (cacheProperties.getRedis().isEnableStatistics()) {
builder.enableStatistics();
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> {
customizer.customize(builder);
});
// 帮我们初始化所有的缓存
return (RedisCacheManager)cacheManagerCustomizers.customize(builder.build());
}
private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
RedisCacheConfiguration
我们自定义配置的注入的容器是这个,名字和上面一样,但是包不一样
package org.springframework.data.redis.cache;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
public class RedisCacheConfiguration {
private final Duration ttl;
private final boolean cacheNullValues;
private final CacheKeyPrefix keyPrefix;
private final boolean usePrefix;
private final SerializationPair<String> keySerializationPair;
private final SerializationPair<Object> valueSerializationPair;
private final ConversionService conversionService;
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
this.ttl = ttl;
this.cacheNullValues = cacheNullValues;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
this.valueSerializationPair = valueSerializationPair;
this.conversionService = conversionService;
}
public static RedisCacheConfiguration defaultCacheConfig() {
return defaultCacheConfig((ClassLoader)null);
}
自定义配置
- 指定生成的缓存使用的key
key属性指定,接收一个SpEL - 指定缓存的数据的存活时间:配置文件修改
- 将数据保存为json格式
将数据保存为json格式
自定义RedisCacheConfiguration即可
package com.atlinxi.gulimall.product.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
*
* Spring Cache默认配置的原理
*
* CacheAutoConfiguration(缓存配置类)-> 帮我们导入了 RedisCacheConfiguration,-> 它又帮我们自动配置了RedisCacheManager(缓存管理器)
* -> 初始化所有的缓存 -> 每个缓存决定使用什么配置 ->如果RedisCacheConfiguration有就用已有的,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可
* -> 就会应用到RedisCacheManager管理的所有缓存分区中
*/
// 开启属性配置的绑定功能,相当于让这个类的绑定配置文件生效
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
// 开启缓存
@EnableCaching
public class MyCacheConfig {
// 因为已经注入容器了,所以可以这么用
// @Autowired
// CacheProperties cacheProperties;
/**
*
*
*1. 原来(源代码)中配置文件绑定的配置类是这样子的
* 只是说spring.cache这个文件绑定
* 并没有放到容器中,我们不能直接使用
* @ConfigurationProperties(
* prefix = "spring.cache"
* )
* public class CacheProperties {
*
*
* 解决用这个
* EnableConfigurationProperties
*
*
*
*
* @return
*/
// @Bean 将结果放入容器
// 方法中的所有参数都会从容器中确定,和@Autowired效果一样
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
// 照着RedisCacheConfiguration写就行
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 因为源码中每次修改完之后,都会返回一个新的对象,所以只能这么写,不能链式调用
// SerializationPair redis序列化器
// 将redis的key序列化器保持原样,还是string,value序列化器使用spring提供的jackson,json数据
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.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
springCache不足
-
读模式
- 缓存穿透:查询一个null数据。解决:缓存空数据,spring.cache.redis.cache-null-values=true
- 缓存击穿:大量并发同时查询一个正好过期的数据,使大量并发同时落到数据库。解决:加锁,springCache默认没有为我们加锁
@Cacheable(value = “category”,key = “#root.methodName”,sync=true)
sync,springCache源码中是使用synchronized
进行加锁,加的是本地锁,而非分布式锁,但关系不大,即使集群有100台机器,也才查一百次而已 - 缓存雪崩:大量的key同时过期。
在超大型系统里面可能会存在,但在一般应用中,即使key同时过期,只要不是十几万个key同时过期,就不需要考虑这个问题。
解决:加随机时间,很容易弄巧成拙,比如a数据3s过期,随机时间为1s,b数据2s过期,随机时间为2s,不加随机时间,失效时间没有冲撞在一起,加了之后反而冲撞了。
真正解决
:加过期时间即可,因为我们存储的时间是不一样的。spring.cache.redis.time-to-live=3600000
-
写模式:(缓存与数据库一致)
- 读写加锁。适用于读多写少的场景,加锁是影响效率的
- 引入Canal,感知到Mysql的更新去更新缓存
- 读多写多,直接去数据库查询就行
总结:
- 常规数据(读多写少,即时性,一致性要求不高的数据),完全可以使用springCache
写模式(只要缓存的数据有过期时间就足够了) - 特殊数据:特殊设计
我觉得不管是太太,小孩也是,任何关系,尤其是夫妻关系,我觉得爱还没有尊重重要,当你不尊敬对方的时候,那关系就很难维持
那个尊敬,不管你是大牌小牌,卑微的人还是你做的不对的时候,在道德上面拿不到标准,你没有努力的时候,你懒散的时候,你敷衍的时候,讲谎话的时候,他不尊重就是不尊重,从眼神看的出来,不是态度,你也不能去逼人家,这都很自然的,包括你小孩要看你就似乎很自然的
李安