大型商城:缓存与分布式锁
整合Redis
1.product中引入Redis依赖
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.yml中配置Redis的地址
spring:
redis:
host: 192.168.56.10
port: 6379
3.test
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
//保存
ops.set("hello","word"+ UUID.randomUUID().toString());
String hello = ops.get("hello");
System.out.println("数据:"+hello);
}
缓存使用-改造三级分类业务
将原来的getCatalogJson方法改为getCatalogJsonFromDb,再新写一个getCatalogJson方法!
思路就是先去Redis中查
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson(){
//给缓存中放入json字符串,拿出的json字符串,还用逆转为能用的对象类型: 【序列化与反序列化】
//1.加入缓存逻辑,缓存中存的数据是json字符串
//json夸语言 夸平台兼容
String catalogJSON = (String) redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON)){
//缓存中没有 查询数据库
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
//查询到数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJSON",s);
return catalogJsonFromDb;
}
//转为我们指定的对象
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
lettuce堆外内存溢出bug
当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError
产生原因:
1)、springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信
2)、lettuce的bug导致netty堆外内存溢出。netty如果没有指定堆外内存,默认使用Xms的值,可以使用-Dio.netty.maxDirectMemory进行设置
解决方案:由于是lettuce的bug造成,不要直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存,治标不治本。
1)、升级lettuce客户端。但是没有解决的
2)、切换使用jedis
<!--Redis-->
<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>
lettuce和jedis是操作redis的底层客户端,RedisTemplate是再次封装
再次进行测试发现没有异常!
缓存失效
关于缓存穿透,缓存雪崩,缓存击穿:https://www.tinstu.com/1714.html
缓存穿透:查询一个不存在的数据,缓存没有,数据库也没有,也不会缓存为null,就会一直查询数据库!
- 对空结果进行缓存
缓存雪崩:key的过期时间相同,导致缓存在某一时刻同时失效,请求全部转发数据库
- 设置过期时间(加随机值),解决缓存雪崩
缓存击穿:某个热点key过期,导致高并发都去查数据库
- 加锁解决缓存穿透
加锁解决缓存击穿的问题
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
不好的方法是synchronized(this),肯定不能这么写 ,不具体写了 本地锁 不适用于分布式
锁时序问题:
之前逻辑是调用这个方法,加锁,查缓存,空就查数据库,释放锁,进行缓存(没有缓存完成之前,可能有一个线程进入,查不到缓存,又去查数据库)
解决:缓存完成再释放锁
查缓存
null->锁
synchronized(this){
查缓存
null->数据库
进行缓存
}
此为本地锁,只能锁定当前进程,分布下无法使用
分布式锁的原理与使用
redis常用命令:https://www.tinstu.com/1506.html
redis分布式锁:https://www.tinstu.com/1719.html
阶段一
使用redis的set k v nx
表示当数据库中key不存在时,可以将key-value添加数据库
阶段二
上面遇到的问题,加锁成功后,程序异常了,锁没有删除,那么程序再启动的时候永久都是加锁失败了,形成死锁!
那么需要给锁设置一个过期时间,加锁和过期时间必须同步,保持原子性,否则还有可能形成死锁的情况!
boollean lock = redisTemplate.opsForValue( ).setIfAbsent("lock","111",300,TimeUnit.SECONDS);
阶段三
如果由于业务很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了
解决:占有锁的时候,值指定为UUID,每个人匹配的是自己的锁才删除!
阶段四
如正好正好判断当前值,正要删除的时候,锁已经过期了,别人已经设置到了新的值,那么我们删除是别人的锁
解决:删除锁必须保证原子性,使用redis+Lua脚本完成!
阶段五 - 最终形态
解锁lua脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
esle
return 0
end
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性,更难的事情,锁的自动续期!
更专业的分布式锁框架 Redission :https://www.tinstu.com/2642.html