SpringBoot2整合Redis从入门到进阶

首先需要安装Redis,如何安装可以看我的这篇文章
接下来说明如何使用,以及一些Redis的相关知识。

一、缓存

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高
  • 访问量大且更新频率不高的数据(读多,写少)

本地缓存:和微服务同一个进程。缺点:分布式时本地缓存不能共享
分布式缓存:缓存中间件

二、使用

1. pom.xml文件中导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. application.yml配置redis

配置redis主机地址

spring:
  redis:
    host: xxx(你的ip)
    port: 6379

3. 自动注入RedisTemplate

public class RedisTests{
	@Autowired
	StringRedisTemplate stringRedisTemplate;

	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);
	}
}

三、改造实例

public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{
	@Autowired
	CategoryBrandRelationService categoryBrandRelationService;
	@Autowired
	private StringRedisTemplate redisTemplate;
	
	@Override
	private Map<String, List<Catalog2Vo>> getCatalogJson(){
		// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
		// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
		String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
		if(StringUtils.isEmpty(catalogJSON )){
			// 2. 缓存中没有,查询数据库
			Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
			// 3. 查到的数据再放入缓存,将对象转为json放入缓存中
			// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
			String s = JSON.toJSONString(catalogJsonFromDb);
			redisTemplate.opsForValue().set("catalogJSON", s);
			return catalogJsonFromDb;
		}
		// 转为指定的对象
		Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
		return result;
	}
	
	// 从数据库查询并封装分类数据
	private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){
		List<CategoryEntity> categoryEntities = this.list();
        //查出所有一级分类
    	List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
    	Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
            //遍历查找出二级分类
        List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
        List<Catalog2Vo> catalog2Vos=null;
        if (level2Categories!=null){
             //封装二级分类到vo并且查出其中的三级分类
             catalog2Vos = level2Categories.stream().map(cat -> {
                //遍历查出三级分类并封装
                List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
                List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
                if (level3Catagories != null) {
                    catalog3Vos = level3Catagories.stream()
                             .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
                            .collect(Collectors.toList());
                }
                Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
        }));
        return listMap;
	}
}

四、缓存失效问题

在这里插入图片描述

1. 缓存穿透

缓存穿透:查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无此记录,没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

风险:利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃。

解决:缓存空对象、布隆过滤器、mvc拦截器

2. 缓存雪崩

缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案:
规避雪崩:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存数据库中。
设置热点数据永远不过期。

出现雪崩:降级 熔断
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存

3. 缓存击穿

缓存雪崩和缓存击穿不同的是:

缓存击穿 指 并发查同一条数据。缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:
设置热点数据永远不过期。
加互斥锁:业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db去数据库加载,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

缓存击穿:加锁
不好的方法是synchronized(this),肯定不能这么写 ,不具体写了。

锁时序问题:之前的逻辑是查缓存没有,然后取竞争锁查数据库,这样就造成多次查数据库。

解决方法:竞争到锁后,再次确认缓存中没有,再去查数据库。

五、修改代码,解决缓存失效问题

1. 非分布式解决方案

public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{
	@Autowired
	CategoryBrandRelationService categoryBrandRelationService;
	@Autowired
	private StringRedisTemplate redisTemplate;
	
	@Override
	private Map<String, List<Catalog2Vo>> getCatalogJson(){
		// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
		/*
			1. 空结果缓存:解决缓存穿透
			2. 设置过期时间(加随机值):解决缓存雪崩
			3. 加锁:解决缓存击穿
		*/
		// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
		String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
		if(StringUtils.isEmpty(catalogJSON )){
			// 2. 缓存中没有,查询数据库
			Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
			return catalogJsonFromDb;
		}
		// 转为指定的对象
		Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
		return result;
	}
	
	// 从数据库查询并封装分类数据
	private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){
		// 在非分布式的场景下,只要是同一把锁,就能锁住需要这个锁的所有线程
		// synchronized(this): SpringBoot所有的组件在容器中都是单例的。
		synchronized(this){
			// 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询
			String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
			if(!StringUtils.isEmpty(catalogJSON)){
				// 如果不为null,直接返回
				Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
				return result;
			}
			List<CategoryEntity> categoryEntities = this.list();
	        //查出所有一级分类
	        List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
	        Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
	            //遍历查找出二级分类
	            List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
	            List<Catalog2Vo> catalog2Vos=null;
	            if (level2Categories!=null){
	                //封装二级分类到vo并且查出其中的三级分类
	                catalog2Vos = level2Categories.stream().map(cat -> {
	                    //遍历查出三级分类并封装
	                    List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
	                    List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
	                    if (level3Catagories != null) {
	                        catalog3Vos = level3Catagories.stream()
	                                .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
	                                .collect(Collectors.toList());
	                    }
	                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
	                    return catalog2Vo;
	                }).collect(Collectors.toList());
	            }
	            return catalog2Vos;
	        }));
	        // 3. 查到的数据再放入缓存,将对象转为json放入缓存中
			// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
			String s = JSON.toJSONString(listMap);
			redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
	        return listMap;
		}
	}
}

2. 分布式解决方案

分布式锁
分布式项目时,但本地锁只能锁住当前服务,需要分布式锁。

redis分布式锁的原理:setnx,同一时刻只能设置成功一个。

前提:锁的key是一定的,value可以变。

没获取到锁阻塞或者sleep一会,设置好了锁,玩意服务出现宕机,没有执行删除锁逻辑,这就造成了死锁

解决:设置过期时间业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁。

解决:锁续期(redisson有看门狗)。删锁的时候明确是自己的锁。如uuid判断uuid对了,但是将要删除的时候锁过期了,别人设置了新值,那删除了别人的锁。

解决:删除锁必须保证原子性(保证判断和删锁是原子的)。使用redis+Lua脚本完成,脚本是原子的。

public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{
	@Autowired
	CategoryBrandRelationService categoryBrandRelationService;
	@Autowired
	private StringRedisTemplate redisTemplate;
	
	@Override
	private Map<String, List<Catalog2Vo>> getCatalogJson(){
		// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
		/*
			1. 空结果缓存:解决缓存穿透
			2. 设置过期时间(加随机值):解决缓存雪崩
			3. 加锁:解决缓存击穿
		*/
		// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
		String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
		if(StringUtils.isEmpty(catalogJSON )){
			// 2. 缓存中没有,查询数据库
			Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
			return catalogJsonFromDb;
		}
		// 转为指定的对象
		Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
		return result;
	}
	
	// 从数据库查询并封装分类数据
	private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock(){
		// 1) 占分布式锁
		String uuid = UUID.randomUUID().toString();
		Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
		if(lock){
			// 加锁成功
			// 2) 设置过期时间,必须和加锁是同步的,原子性的
			// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
			Map<String, List<Catalog2Vo>> dataFromDb;
			try{
				dataFromDb = getDataFromDb();
			}finally{
				// get与delete的原子操作
				String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
	            "    return redis.call(\"del\",KEYS[1])\n" +
	            "else\n" +
	            "    return 0\n" +
	            "end";
	            Long lockValue = stringRedisTemplate.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{
			// 加锁失败
			// 休眠 100ms
			try {
            	Thread.sleep(100);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
			return getCatalogJsonFromDbWithRedisLock(); // 自旋的方式
		}
		return getDataFromDb();
	}

	private Map<String, List<Catalog2Vo>> getDataFromDb(){
		// 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询
		String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
		if(!StringUtils.isEmpty(catalogJSON)){
			// 如果不为null,直接返回
			Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
			return result;
		}
		List<CategoryEntity> categoryEntities = this.list();
	    //查出所有一级分类
		List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
		Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
	            //遍历查找出二级分类
			List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
			List<Catalog2Vo> catalog2Vos=null;
			if (level2Categories!=null){
				//封装二级分类到vo并且查出其中的三级分类
				catalog2Vos = level2Categories.stream().map(cat -> {
					//遍历查出三级分类并封装
					List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
					List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
					if (level3Catagories != null) {
						catalog3Vos = level3Catagories.stream()
								.map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
								.collect(Collectors.toList());
		            }
		            Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
		            return catalog2Vo;
	            }).collect(Collectors.toList());
			}
			return catalog2Vos;
		}));
	    // 3. 查到的数据再放入缓存,将对象转为json放入缓存中
		// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
		String s = JSON.toJSONString(listMap);
		redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
	    return listMap;	
	}
}

六、使用更专业的分布式锁框架Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

1. 环境搭建

1)pom.xml导入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.15.6</version>
</dependency>

2)yaml文件配置

application.yml

spring:
  application:
    name: springboot-redisson
  redis:
    redisson:
      file: classpath:redisson.yml

redisson.yml

# 单节点配置
singleServerConfig:
  # 连接空闲超时,单位:毫秒
  idleConnectionTimeout: 10000
  # 连接超时,单位:毫秒
  connectTimeout: 10000
  # 命令等待超时,单位:毫秒
  timeout: 3000
  # 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。
  # 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。
  retryAttempts: 3
  # 命令重试发送时间间隔,单位:毫秒
  retryInterval: 1500
  #  # 重新连接时间间隔,单位:毫秒
  #  reconnectionTimeout: 3000
  #  # 执行失败最大次数
  #  failedAttempts: 3
  # 密码
  password: 1234
  # 单个连接最大订阅数量
  subscriptionsPerConnection: 5
  # 客户端名称
  clientName: null
  #  # 节点地址
  address: "redis://127.0.0.1:6379"
  # 发布和订阅连接的最小空闲连接数
  subscriptionConnectionMinimumIdleSize: 1
  # 发布和订阅连接池大小
  subscriptionConnectionPoolSize: 50
  # 最小空闲连接数
  connectionMinimumIdleSize: 500
  # 连接池大小
  connectionPoolSize: 1000
  # 数据库编号
  database: 0
  # DNS监测时间间隔,单位:毫秒
  dnsMonitoringInterval: 5000
# 线程池数量,默认值: 当前处理核数量 * 2
threads: 16
# Netty线程池数量,默认值: 当前处理核数量 * 2
nettyThreads: 32
# 编码,不使用默认编码,因为set进去之后是乱码
#codec: !<org.redisson.codec.MarshallingCodec> {}
# 传输模式
transportMode : "NIO"

3)测试

import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootRedissonApplicationTests {

    @Autowired
    private RedissonClient redissonClient;

    @Test
    void contextLoads() {
        redissonClient.getBucket("hello").set("bug");
       	String test = (String) redissonClient.getBucket("hello").get();
        System.out.println(test);
    }
}

具体使用方式可以查看官网,有详细的说明

2. 相关知识

  1. 锁的自动续期,如果业务时间超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期被删掉。
  2. 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。
  3. 如果传递了锁的超时时间,就发送给redis执行脚本,进行占领,默认超时就是指定的时间。
  4. 如果未指定锁的超时时间,就使用30 * 1000 (LockWatchdogTimeout看门狗的默认时间)。
  5. 只要占锁成功,就会启动一个定时任务(重新给锁设置过期时间,新的过期时间就是看门狗的默认时间)。

3. 最终代码

public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redisson;

    @Override
    private Map<String, List<Catalog2Vo>> getCatalogJson() {
        // 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
		/*
			1. 空结果缓存:解决缓存穿透
			2. 设置过期时间(加随机值):解决缓存雪崩
			3. 加锁:解决缓存击穿
		*/
        // 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            // 2. 缓存中没有,查询数据库
            Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();
            return catalogJsonFromDb;
        }
        // 转为指定的对象
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });
        return result;
    }

    /*
    * 缓存里面的数据如何和数据库保持一致
    * 缓存数据的一致性
    * 1)双写模式:写完数据库后,马上写到缓存里。
    *       缺点:脏数据问题,这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
    * 2)失效模式:写完数据库后,马上删掉缓存的数据。
    *       缺点:也会产生脏数据问题。
    * 解决办法:1. 对于实时性要求高的数据,直接读数据库。
    *         2. 使用读写锁(业务不关心脏数据,允许临时脏数据可忽略)
    *         3. 缓存数据一致性-解决-Canal
    */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        // 1. 锁的名字。(锁的粒度,越细越快)
        // 锁的粒度:具体缓存的是某个数据,例11号商品:product-11-lock
        redisson.getLock("catalogJson-lock");
        lock.lock();
        Map<String, List<Catalog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            lock.unlock();
        }
        return dataFromDb;
    }

    private Map<String, List<Catalog2Vo>> getDataFromDb() {
        // 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            // 如果不为null,直接返回
            Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });
            return result;
        }
        List<CategoryEntity> categoryEntities = this.list();
        //查出所有一级分类
        List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
        Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //遍历查找出二级分类
            List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
            List<Catalog2Vo> catalog2Vos = null;
            if (level2Categories != null) {
                //封装二级分类到vo并且查出其中的三级分类
                catalog2Vos = level2Categories.stream().map(cat -> {
                    //遍历查出三级分类并封装
                    List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
                    List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
                    if (level3Catagories != null) {
                        catalog3Vos = level3Catagories.stream()
                                .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
                                .collect(Collectors.toList());
                    }
                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        // 3. 查到的数据再放入缓存,将对象转为json放入缓存中
        // 使用alibaba的fastjson包,可以将任意对象转换为json字符串
        String s = JSON.toJSONString(listMap);
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
        return listMap;
    }
}

七、缓存框架Spring Cache(可选)注:没有分布式锁

首先理一下:

  • Redis是缓存,Spring Cache 能对 Redis 进行注解式操作
  • redisson 是分布式锁

每次都那样写缓存太麻烦了,spring从3.1开始定义了org.springframework.cache.Cache org.springframework.cache.CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发。

Cache接口的实现包括RedisCache、EhCacheCache、ConcurrentMapCache等。

每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点:

  • 确定方法需要缓存以及他们的缓存策略
  • 从缓存中读取之前缓存存储的数据

1. 导入依赖

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2. 写配置

spring:
  cache:
  	#指定缓存类型为redis
    type: redis
    redis:
      # 指定redis中的过期时间为1h
      time-to-live: 3600000

3. 使用

  • @Cacheable: 触发将数据保存到缓存的操作
  • @CacheEvict: 触发将数据从缓存删除的操作
  • @CachePut: 不影响方法执行更新缓存
  • @Caching: 组合以上多个操作
  • @CacheConfig: 在类级别共享缓存的相同配置

1) 开启缓存功能

在主类上加上注解

@EnableCaching

2) 只需要使用注解就能完成缓存操作

在方法上注解:代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。反之则调用方法,最后将方法的结果放入缓存。
每一个需要缓存的数据都来指定要放到哪个名字的缓存。(缓存的分区 [按照业务类型分] )

默认行为

  • key默认自动生成:缓存名字::SimpleKey [] (自主生成的key值)
  • 缓存的value值,默认使用jdk序列化机制。将序列化后的数据存到redis
  • 默认 ttl 时间:-1
@Cacheable({"category"})
@Override
public List<CategoryEntity> getLevel1Categorys(){
	List<CategoryEntity> categoryEntities = baseMapper.selectList();
	return categoryEntities;
}

自定义

  • 指定生成的缓存所使用的key:key属性指定,接受一个SpEL
  • 指定缓存数据的存活时间:配置项目中修改TTL
  • 将数据保存为JSON格式:CacheAutoConfiguration,RedisCacheConfiguration
@Cacheable({"category"}, key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys(){
	List<CategoryEntity> categoryEntities = baseMapper.selectList();
	return categoryEntities;
}

4. 原理

  • CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置 -> 如果redisCacheConfiguration有就用已有的,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可 -> 就会应用到当前RedisCacheManager管理的所有缓存分区中。

5. 自动配置类(扩展)

@Configuration
public class MyCacheConfig {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
            .defaultCacheConfig();
        //指定缓存序列化方式为json
        config = config.serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        //设置配置文件中的各项配置,如过期时间
        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;
    }
}

6. @CacheEvict

第一个方法存放缓存,第二个方法清空缓存

// 调用该方法时会将结果缓存,缓存名为category,key为方法名
// sync表示该方法的缓存被读取时会加锁 // value等同于cacheNames // key如果是字符串"''"
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
    return getCategoriesDb();
}

//调用该方法会删除缓存category下的所有cache,如果要删除某个具体,用key="''"
@Override
@CacheEvict(value = {"category"},allEntries = true)
public void updateCascade(CategoryEntity category) {
    this.updateById(category);
    if (!StringUtils.isEmpty(category.getName())) {
        categoryBrandRelationService.updateCategory(category);
    }
}

如果要清空多个缓存,用@Caching(evict={@CacheEvict(value=“”)})

7. 不足

1)读模式

缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。
2) 写模式:(缓存与数据库一致)

读写加锁。
引入Canal,感知到MySQL的更新去更新Redis
读多写多,直接去数据库查询就行
3)总结:

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):

写模式(只要缓存的数据有过期时间就足够了)

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值