高级-05缓存与分布式锁
一、缓存
1、缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落
盘工作。
哪些数据适合放入缓存?
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率
来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。

data = cache.load(id); // 从缓存加载数据
if(data == null){
data = db.load(id); // 从数据库中加载数据
cache.put(id,data); // 保存到cache中
}
return data;
注意:在开发中,凡是放入缓存中的数据都应该指定过期时间,使其可以在系统即使没
有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题。
2、本地缓存
// 全局变量
private Map<String,Object> cache = new HashMap<>();
本地缓存在分布式下的问题

3、分布式缓存

4、整合redis

(1)引入redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// 版本号由父项目控制
(2)简单配置redis的host等信息
spring:
redis:
host: 192.168.56.10
port: 6379

(3)使用redis自动配置好的StringRedisTemplate来操作redis
(4)redis使用测试
@Autowired
StringRedisTemplate stringRedisTemplate;
public void test() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 保存
ops.set("hello", "world_" + UUID.randomUUID().toString());
// 查询
String hello = ops.get("hello");
System.out.println(hello);
}
缓存中一般存json字符串(json跨语言、跨平台兼容)
// fastjson序列化 JSON.toJSONString(catelogVo); // fastjson反序列化 JSONObject.parseObject("",new TypeReference<Map<String,List<Catelog>>>(){});
(5)redis可视化界面工具

5、对外内存溢出OutOfDirectMemoryError
SpringBoot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
压力测试时,lettuce的bug会导致netty堆外内存溢出。
可以使用 -Dio.netty.maxDirectMemory 设置netty对外内存
如果没有单独指定内存大小 默认使用 -Xmx ***m 参数


》》》》》》》 解决方案《《《《《《《
不能使用 -Dio.netty.maxDirectMemory 只去调大堆外内存。(只会延长异常出现时间)
- 升级lettuce客户端
- 切换使用jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--排除lettuce-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入jedis-->
<!--点进spring-boot-starter-parent、spring-boot-dependencies,确认jedis版本是否存在-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
二、缓存失效问题
1、缓存穿透

2、缓存雪崩

3、缓存击穿

4、解决问题
4.1 空结果缓存,解决缓存击穿
// 获取业务数据data
Object data = getData();
if(StringUtils.isEmpty(data)){
// 查询不到数据就放一个特定的值
stringRedisTemplate.opsForValue().set("id", "特定的值");
}
// 有值就放进来
stringRedisTemplate.opsForValue().set("id", JSONObject.toJSONString(data));
4.2 设置多起时间(加随机值),解决缓存雪崩

stringRedisTemplate.opsForValue()
.set("id", JSONObject.toJSONString(data),
60 + new Random().nextInt(600), TimeUnit.MINUTES);
4.3 加锁,解决缓存击穿
单机应用下可以对当前对象加锁
但:分布式应用高并发下有X个应用就有可能同时进来X个线程查询同一条数据


本地锁: synchronized,JUC(Lock),只能锁住本地线程。在分布式情况下,想要锁住所有,必须使用分布式锁。
5 锁-时序问题
由于查询完数据库释放锁到结果写入缓存之间存在时间间隔,会导致有多个线程查询数据库
所以需要在缓存写入操作完成后再释放锁。

6 附录
6.1 IDEA复制应用

6.2 IDEA设置运行端口号

三、分布式锁介绍

只有一个能成功,这就是分布式锁的原理
1、基本原理

2、redis分布式锁演进






最重要的两个问题
1、原子加锁》》》set NX EX
2、原子解锁》》》lua脚本
public void getRedisLock() {
// 占分布式锁,去redis占坑
// 设置过期时间(必须和加锁是同步的、原子的),防止意外事故导致无法及时删除锁
// 增加UUID防止删除别人的锁
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("Lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
// 加锁成功...执行业务
try {
doData();
} finally {
// 业务处理完删除锁
// String lockValue = stringRedisTemplate.opsForValue().get("Lock");
// if(uuid.equals(lockValue)){
// // 删除自己的锁
// stringRedisTemplate.delete("Lock");
// }
// 获取值对比+对比成功(中途可能耗时导致锁过期) 也必须是原子操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
// 原子删锁
Long lock1 = stringRedisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList("Lock"), uuid);
}
} else {
// 加锁失败...
// 休眠100ms重试
try{
Thread.sleep(200);
}catch (Exception e){
}
getRedisLock(); // 自旋的方式
}
}
这只是一个简单实现的redis分布式锁
实际上redis官方并不推荐这种方法,市面上有许多成熟的完整的分布式锁应用
而redis官方推荐的是redisson


四、缓存数据一致性
1、双写模式

2、失效模式

3、解决方案

4、Canal
缺点:需要多开发一个中间件
优点:一次开发,后续就不用管了


浙公网安备 33010602011771号