缓存的使用(Redis、Redisson、SpringCache)

本地简单使用缓存

//本地缓存,通过hashmap简单示例
private Map<String,Object> cache=new HashMap<>();
public Object getJsonData() {
	if(!cache.containsKey("jsonCache")){
		//执行查询业务
		Object object = null;
		cache.put("jsonCache",object);
		return object;
	}
	return cache.get("jsonCache");
}

以上的方式只是简单的缓存使用,并不适用于多机器一起运行的情况

整合Redis

导入匹配的版本依赖(如果有父pom文件管理版本就无需指定版本)

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

配置redis主机地址(暂时使用单机,后续可以使用多节点、哨兵模式、集群模式)

spring:
  redis:
    host: yourIp
    port: 6379

后续在spring中可以直接使用两个对象

	//需要自己定义下数据的序列化使用
	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	//提供了str、hash中key及valye的序列化为string
	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

在项目中的简单验证

	@Autowired
    private StringRedisTemplate redisTemplate;
    public void testRedisTemplate(){
        redisTemplate.opsForValue().set("hello","world");
        String hello = redisTemplate.opsForValue().get("hello");
        System.out.println(hello);
    }

redis压力测试下存在OutOfDirectMemoryError问题

产生原因:
    1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用通信
    2)、lettuce的bug导致netty堆外内存溢出
    解决方案:由于是lettuce的bug造成,不能直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存
    1)、升级lettuce客户端。 2)、切换使用jedis

	<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>
	<dependency>
		<groupId>redis.clients</groupId>
		<artifactId>jedis</artifactId>
	</dependency>

缓存不正确使用造成的问题

1)缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,
这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决: null结果缓存,并加入短暂过期时间
2)缓存雪崩
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时 压力过重雪崩。
解决: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3)缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决: 加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

独立运行机器加锁解决方式(无法解决分布式的情况)

    @Autowired
    private StringRedisTemplate redisTemplate;
    public String testRedisTemplate2(){
        String hello = redisTemplate.opsForValue().get("hello");
        if(StringUtil.isBlank(hello)){
            synchronized (this){
                //二次查询缓存 保证在多线程情况下只查询一次数据库
                hello = redisTemplate.opsForValue().get("hello");
                if(StringUtil.isNotBlank(hello)){
                    return hello;
                }
                //TODO 自己的业务逻辑
                String value = "world";
                //放入缓存中
                redisTemplate.opsForValue().set("hello",value);
            }
        }
        return hello;
    }

redis锁解决分布式的方式

方式一

@Autowired
    private StringRedisTemplate redisTemplate;
    public String testRedisTemplateRedisLock(){
        String hello = redisTemplate.opsForValue().get("hello");
        if(StringUtil.isBlank(hello)){
            return getDBData();
        }
        return hello;
    }
    public String getDBData() {
        // 如果不存在lock中key时就进行创建,存在创建失败
        // 存在问题:1、如果执行到自己的业务逻辑时系统中断,导致lock一直存在,那么就会产生死锁
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", "1");
        if (aBoolean) {
            try {
                //二次查询缓存
                String hello = redisTemplate.opsForValue().get("hello");
                if(StringUtil.isNotBlank(hello)){
                    return hello;
                }
                //TODO 自己的业务逻辑
                String value = "world";
                //放入缓存中
                redisTemplate.opsForValue().set("hello",value);
                return value;
            }finally {
                redisTemplate.delete("lock");
            }
        }else{
            // 自旋调用
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getDBData();
        }
    }

方式二

	@Autowired
    private StringRedisTemplate redisTemplate;
    public String testRedisTemplateRedisLock(){
        String hello = redisTemplate.opsForValue().get("hello");
        if(StringUtil.isBlank(hello)){
            return getDBData2();
        }
        return hello;
    }
    public String getDBData2() {
        // 如果不存在lock中key时就进行创建,存在创建失败,并设置过期时间
        // 存在问题:1、如果执行到自己的业务时间超过锁的过期时间,就是业务还在执行,但是锁已经过期,就会导致锁失效(后续可以锁续期)
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", "1",1000, TimeUnit.MICROSECONDS);
        if (aBoolean) {
            try {
                //二次查询缓存
                String hello = redisTemplate.opsForValue().get("hello");
                if(StringUtil.isNotBlank(hello)){
                    return hello;
                }
                //TODO 自己的业务逻辑
                String value = "world";
                //放入缓存中
                redisTemplate.opsForValue().set("hello",value);
                return value;
            }finally {
                redisTemplate.delete("lock");
            }
        }else{
            // 自旋调用
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getDBData();
        }
    }

方式三

	@Autowired
    private StringRedisTemplate redisTemplate;
    public String testRedisTemplateRedisLock(){
        String hello = redisTemplate.opsForValue().get("hello");
        if(StringUtil.isBlank(hello)){
            return getDBData2();
        }
        return hello;
    }
    public String getDBData3() {
        //增加唯一ID。保证如果是自己加了锁,只有自己才可以关闭
        String s = UUID.randomUUID().toString();
        // 如果不存在lock中key时就进行创建,存在创建失败,并设置过期时间
        // 存在问题:1、如果执行到自己的业务时间超过锁的过期时间,就是业务还在执行,但是锁已经过期,就会导致锁失效(后续可以锁续期)
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", s,1000, TimeUnit.MICROSECONDS);
        if (aBoolean) {
            try {
                //二次查询缓存
                String hello = redisTemplate.opsForValue().get("hello");
                if (StringUtil.isNotBlank(hello)) {
                    return hello;
                }
                //TODO 自己的业务逻辑
                String value = "world";
                //放入缓存中
                redisTemplate.opsForValue().set("hello",value);
                return value;
            } finally {
                //但是此处进行移除是非原子性的,redis中一条执行命令才是线程安全的
                String lock = redisTemplate.opsForValue().get("lock");
                if (s.equals(lock)) {
                    redisTemplate.delete("lock");
                }
            }
        }else{
            // 自旋调用
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getDBData();
        }
    }

方式四

	@Autowired
    private StringRedisTemplate redisTemplate;
    public String testRedisTemplateRedisLock(){
        String hello = redisTemplate.opsForValue().get("hello");
        if(StringUtil.isBlank(hello)){
            return getDBData2();
        }
        return hello;
    }
    public String getDBData4() {
        //增加唯一ID。保证如果是自己加了锁,只有自己才可以关闭
        String s = UUID.randomUUID().toString();
        // 如果不存在lock中key时就进行创建,存在创建失败,并设置过期时间
        // 存在问题:1、如果执行到自己的业务时间超过锁的过期时间,就是业务还在执行,但是锁已经过期,就会导致锁失效(后续可以锁续期)
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", s,1000, TimeUnit.MICROSECONDS);
        if (aBoolean) {
            try {
                //二次查询缓存
                String hello = redisTemplate.opsForValue().get("hello");
                if (StringUtil.isNotBlank(hello)) {
                    return hello;
                }
                //TODO 自己的业务逻辑
                String value = "world";
                //放入缓存中
                redisTemplate.opsForValue().set("hello",value);
                return value;
            } finally {
                // 通过lua脚本保证redis执行为线程安全
                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";
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), s);
            }
        }else{
            // 自旋调用
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getDBData();
        }
    }

redisson锁解决分布式的方式

导入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

开启配置

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://yourIP:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

redisson分布式锁

	//Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
	@Autowired
    private RedissonClient redissonClient;
    public String testRedisTemplateRedisLock(){
        String hello = redisTemplate.opsForValue().get("hello");
        if(StringUtil.isBlank(hello)){
            return getDBData2();
        }
        return hello;
    }
    public String getDBData4() {
		RLock lock = redissonClient.getLock("getDBData4-Lock");
        try {
			//二次查询缓存
			String hello = redisTemplate.opsForValue().get("hello");
			if (StringUtil.isNotBlank(hello)) {
				return hello;
			}
			//TODO 自己的业务逻辑
			String value = "world";
			//放入缓存中
			redisTemplate.opsForValue().set("hello",value);
			return value;
		} finally {
			lock.unlock();
		}
    }

可重入锁(Reentrant Lock)

    public String getDBData4() {
		RLock lock = redissonClient.getLock("getDBData4-Lock");
        try {
			//二次查询缓存
			String hello = redisTemplate.opsForValue().get("hello");
			if (StringUtil.isNotBlank(hello)) {
				return hello;
			}
			//TODO 自己的业务逻辑
			String value = "world";
			//放入缓存中
			redisTemplate.opsForValue().set("hello",value);
			return value;
		} finally {
			lock.unlock();
		}
    }

读写锁(ReadWriteLock)

	// 写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁
	// 总之含有写的过程都会被阻塞,只有读读不会被阻塞
	// 如果在读中有写锁触发、那么写锁会等待读锁执行完后再执行。
    @GetMapping("/read")
    @ResponseBody
    public String read() {
        RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
        RLock rLock = lock.readLock();
        String s = "";
        try {
            rLock.lock();
            System.out.println("读锁加锁"+Thread.currentThread().getId());
            Thread.sleep(5000);
            s= redisTemplate.opsForValue().get("lock-value");
        }finally {
            rLock.unlock();
            return "读取完成:"+s;
        }
    }

    @GetMapping("/write")
    @ResponseBody
    public String write() {
        RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
        RLock wLock = lock.writeLock();
        String s = UUID.randomUUID().toString();
        try {
            wLock.lock();
            System.out.println("写锁加锁"+Thread.currentThread().getId());
            Thread.sleep(10000);
            redisTemplate.opsForValue().set("lock-value",s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            wLock.unlock();
            return "写入完成:"+s;
        }
    }

信号量(Semaphore)

// 可以理解为在一个临界区内可以允许N个资源同时进行,资源为0时,临界期不允许进入了
@GetMapping("/park")
@ResponseBody
public String park() {
    RSemaphore park = redissonClient.getSemaphore("park");
    try {
        park.acquire(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "停进2";
}

@GetMapping("/go")
@ResponseBody
public String go() {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.release(2);
    return "开走2";
}
闭锁(CountDownLatch)
// 可以理解为放闸的开关,当满足放闸条件时,await就不进行阻塞
@GetMapping("/setLatch")
@ResponseBody
public String setLatch() {
	RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
	try {
		latch.trySetCount(5);
		latch.await();
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return "门栓被放开";
}

@GetMapping("/offLatch")
@ResponseBody
public String offLatch() {
	RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
	latch.countDown();
	return "门栓被放开1";
}

缓存一致性问题

双写模式
    当数据更新时,更新数据库时同时更新缓存
    这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
失效模式
    数据库更新时将缓存删除
    存在问题
    当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,
    在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数据
    解决方法
    1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新 2、读写数据的时候(并且写的不频繁),加上分布式的读写锁。
    2、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
    如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
    缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
    通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略)
总结
    我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
    我们不应该过度设计,增加系统的复杂性
    遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

SpringCache

导入依赖

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

启动springBoot自动装配@EnableCaching(可能新版本需要该注解就自动配置了)


自定义配置

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

默认使用jdk进行序列化,自定义序列化方式需要编写配置类
 

@Configuration
public class MyCacheConfig {
    @Bean
    public org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration(
            CacheProperties cacheProperties) {
        org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
                .defaultCacheConfig();
        //指定缓存序列化方式为json
        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;
    }
}

springCache的使用

  	//调用该方法时会将结果缓存,缓存名为 testType,key为方法名
	//表示该方法的缓存被读取时会加锁(本地锁)
	@Cacheable(value = {"testType"},key = "#root.methodName",sync = true)
    public Map<String, List<ObejctVo>> getJsonDbWithSpringCache() {
        return getJsONDb();
    }

	//调用该方法会删除缓存testType下的所有cache
    @Override
    @CacheEvict(value = {"testType"},allEntries = true)
    public void updateCascade(JsonEntity entity) {
        this.updateById(entity);
        if (!StringUtils.isEmpty(entity.getName())) {
            jsonService.update(entity);
        }
    }

Spring-Cache的不足之处

    1)、读模式
    缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
    缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
    使用sync = true来解决击穿问题
    缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
    2)、写模式:(缓存与数据库一致)
    a、读写加锁。
    b、引入Canal,感知到MySQL的更新去更新Redis
    c 、读多写多,直接去数据库查询就行
    3)、总结:
    常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
    写模式(只要缓存的数据有过期时间就足够了)
    特殊数据:特殊设计

SpringCache注解详情

1、Cache接口:缓存接口,定义缓存操作。实现有 如RedisCache、EhCacheCache、ConcurrentMapCache等
2、cacheResolver:指定获取解析器
3、CacheManager:缓存管理器,管理各种缓存(Cache)组件;如:RedisCacheManager,使用redis作为缓存。指定缓存管理器
4、@Cacheable:
    在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有会调用方法获取数据返回,并缓存起来。
    只是Sqel表达式进行key定义
5、@CacheEvict:将一条或多条数据从缓存中删除。
6、@CachePut:将方法的返回值放到缓存中
7、@EnableCaching:开启缓存注解功能
8、@Caching:组合多个缓存注解;可组合(@Cacheable、@CacheEvict、@CachePut)
9、@CacheConfig:统一配置@Cacheable中的value值

  • 9
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值