缓存与分布式锁
缓存
缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。
哪些数据适合放入缓存?
- 即时性、数据致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少)
- 举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。
整合rediss作为缓存
使用过程
-
导入依赖,导入spring-boot-starter-data-redis 依赖会帮我注入RedisAutoConfiguration这个配置类,这个配置类里面有RedisTemplate<Object, Object> 和 StringRedisTemplate
-
这里不适用lettuce作为redis 的客户端,而是另外导入jedis。
-
因为使用lettuce会出现OutOfDirectMemoryError(堆外内存溢出)
-
1)、springboot2.0 以后默认使用lettuce 作为操作redis的客户端。而lettuce 使用netty进行网络通信。 2)、但是lettuce一直存在bug导致netty堆外内存溢出。而netty如果没有指定堆外内存默认使用jvm参数-Xmx300m 我们也可以通过-Dio.netty.maxDirectMemory进行设置, 解决方案:不能使用 去调大 -Dio.netty.maxDirectMemory 堆外内存,只能修改lettuce源码 1)、设计lettuce客户端(改源码)。2)、切换使用jedis(缺点很久没更新了)
-
概念的理解:
- redisTemplate: 是对redis各个客户端的同一API,方便我们程序员使用
- lettuce、jedis 是操作redis的底层客户端。Spring再次封装成redisTemplate
-
-
配置redis地址信息
-
从IOC 容器中取出Spring 封装好的RedisTemplate
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 排除lettuce 使用jedis 操作redis -->
<exclusions>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
redis:
host: 192.168.1.10
port: 6379
api的使用
public class Xxx{
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void test1(){
stringRedisTemplate.opsForValue().get("catalogJSON");
stringRedisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogs), 1, TimeUnit.DAYS);// 缓存一天
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "xxxxx", 300, TimeUnit.SECONDS);// set lock 11 EX 300 NX
// 定义脚本 http://www.redis.cn/commands/set.html
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
// 泛型是为了序列化结果,这是官方建议的,分布式上锁,最好使用lua脚本来执行
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
// 对象的序列化和反序列化(json)
String json = JSON.toJSONString(catalogs);
Map<String, List<Catelog2Vo>> catalogs = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
}
}
缓存失效问题
概述
缓存穿透:查询一个不存在的商品,如果我们设置查询结果为null就不放入缓存。那么有人恶意攻击发送百万并发查询这个不存在的数据,那么由于我们的业务逻辑是查询不存在的商品就不放入缓存,这就导致这百万并发都会去查询数据库。那么我们设置的缓存就没有任何意义了。解决办法:如果查询的数据为null,我们依然存入缓存但是设置一个有效期。(查询永不存在的数据)
缓存雪崩:就是我们存入缓存的数据同时到了有效期,数据大面积消失,刚好有人查询了这些大面积失效的数据,由于缓存中没有,所以还是会出现大量的数据库操作。解决办法,设置缓存有效期时加上一个随机的时间,保证缓存有效期不一样。(缓存大面积失效)
缓存击穿:某一个数据失效,但是这个数据是热点数据,经常出现百万并发查询。但这个数据失效时如果出现百万并发,那么就会发送百万次数据库操作。解决办法:加锁,同一时间只让一个人去查,后续人拿到锁查询时在缓存就有数据了。(某一热点key消失)
解决缓存失效问题
缓存穿透:允许缓存空数据,给空数据设置短的有效期。
缓存雪崩:每个key设置不同的有效期。
缓存击穿:上锁。本地锁、分布式锁。
缓存数据一致性
缓存数据一致性解决方案:双写模式、失效模式。
canal 是阿里开源的产品,可以模拟成mysql的从数据库。当我们对mysql操作是肯定会产生binlog(二进制日志)。canal就假装从数据库读取到二进制日志。
本地锁
业务逻辑:调用获取数据的方法 -> 查询缓存 -> 没命中缓存 -> 上锁(synchronized)-> 再次查询缓存 -> 查询数据库、将结果存入缓存 -> synchronized代码块结束 -> 返回数据。
// 这里只是上锁的那部分代码
public /*synchronized*/ Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithLocalLock() {
// 只要是同一把锁,就能锁住这个锁的所有线程。
// synchronized (this);SpringBoot 所有的组件在容器中都是单例的,所以同一时刻只有一个线程能访问这个方法,synchronize放在这里是可以实现业务要求的。
// TODO 本地锁:synchronize,JUS(lock),在分布式情况下,想要锁住所有,必须使用分布式锁(分布式锁缺点效率慢)
synchronized (this) {
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
Map<String, List<Catelog2Vo>> collect2 = null;
if (StringUtils.isEmpty(catalogJSON)) {
collect2 = getData();
}
return collect2;
}
}
这里需要注意 查询数据库、将结果存入缓存 需要在锁内执行,保证不会因为网络堵塞的问题,导致还没存入缓存,就被第二个线程进来又查询了数据库。
分布式锁
概述
代码演示
判断再删除的脚本 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
业务逻辑:调用获取数据的方法 -> 查询缓存 -> 没命中缓存 -> 上锁(synchronized)-> 再次查询缓存 -> 查询数据库、将结果存入缓存 -> synchronized代码块结束 -> 返回数据。
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 从缓存获取分类数据
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
Map<String, List<Catelog2Vo>> catalogs = null;
if (StringUtils.isEmpty(catalogJSON)) {
// 缓存没有数据,查询数据库
System.out.println("缓存不命中....查询数据库....");
catalogs = getCatalogJsonFromDBWithRedisLock();
return catalogs;
}
// 将json 数据反序列化为对象
catalogs = JSON.