一、缓存
1.1 那些数据适合缓存
1)即时性,数据一致性要求不要的数据
2)访问量大且更新频率不高的数据(读多,写少)
举例::电商类应用,商品分类
,商品列表
等适合缓存并加一个失效时间(根据数据更新频率
来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
注意:
在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没
有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致
问题。
1.2 缓存的流程
1.2 本地缓存实例
/**
* 本地缓存
*/
private Map<String,Object> mapCache = new HashMap<>();
@ResponseBody
@GetMapping(value = "/cashe")
public String testCashe() {
Object getCatalogJson = mapCache.get("cat");
if(getCatalogJson==null){
// 从数据库查询
getCatalogJson = this.getCatalogJson();
}
return getCatalogJson.toString();
}
1.3 本地缓存在微服务中存在的问题
由于我们的微服务部署时可能不是部署一处,而是部署了多份,通过负载均衡去匹配请求,每一个服务中的本地缓存时不一样的,这样在调用的时候就会有问题。所以在微服务中我们应该使用分布式缓存,而不能使用本地缓存。
1.4 解决本地缓存在微服务中负载均衡不一致问题
把多个微服务的缓存保存在同一个位置,如redis
二、★ 整合 redis 作为缓存
第一步:添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步:添加配置
6379 redis的默认端口,可以不写
spring:
redis:
host: 192.168.56.10
port: 6379
第三步:使用 RedisTemplate 操作 redis
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 保存
ops.set("hello","world");
// 查询
String hello = ops.get("hello");
System.out.println("maruis----保存的是数据时-->" + hello);
}
第三步:StringRedisTemplate的更多用法
https://blog.csdn.net/weixin_43835717/article/details/92802040/
五大常用数据类型使用场景
String
缓存:将数据以字符串方式存储
计数器功能:比如视频播放次数,点赞次数。
共享session:数据共享的功能,redis作为单独的应用软件用来存储一些共享数据供多个实例访问。
字符串的使用空间非常大,可以结合字符串提供的命令充分发挥自己的想象力
hash
字典。键值对集合,即编程语言中的Map类型。适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。适用于:存
储、读取、修改用户属性。也可以用Hash做表数据缓存
list
链表(双向链表),增删快,提供了操作某一段元素的API。适用于:最新消息排行等功能;消息队列。
set
集合。哈希表实现,元素不重复,为集合提供了求交集、并集、差集等操作。适用于:共同好友;利用唯一性,统计访问网站的所有独立ip;> 好友推荐时,根据tag求交集,大于某个阈值就可以推荐。
sorted set
有序集合。将Set中的元素增加一个权重参数score,元素按score有序排列。数据插入集合时,已经进行天然排序。适用于:排行榜;带权重的消息队列。
三、使用Redis的StringRedisTemplate
3.1 添加缓存
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 添加缓存逻辑
* @return
*/
// @Cacheable(value = "category",key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
Map<String, List<Catelog2Vo>> result = null;
// 1、加入缓存
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if(StringUtils.isEmpty(catelogJSON)){
// 如果缓存中有就用缓存的,如果没有就从数据库查,查出后把数据放入缓存
result = getCatalogJsonFromDB();
// 缓存中一般都时村json,因为它可以跨语言,跨数据
stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(result));
}else{
result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
}
return result;
}
3.2 .OutOfDirectMemoryError 堆外内存移除的异常
Caused by: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:725)
at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:680)
at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:772)
at io.netty.buffer.PoolArena$DirectArena.newUnpooledChunk(PoolArena.java:762)
at io.netty.buffer.PoolArena.allocateHuge(PoolArena.java:260)
at io.netty.buffer.PoolArena.allocate(PoolArena.java:232)
at io.netty.buffer.PoolArena.reallocate(PoolArena.java:400)
at io.netty.buffer.PooledByteBuf.capacity(PooledByteBuf.java:119)
at io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:303)
at io.netty.buffer.AbstractByteBuf.ensureWritable(AbstractByteBuf.java:274)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1111)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1104)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1095)
at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:554)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1421)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:697)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:632)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:549)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:511)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:918)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
... 1 more
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
需要 allocate 46137344 物理内存 (使用了: 58720256, 最大: 100663296),100m-58m<42m 不够用造成的
这里时100m时lettuce再底层拿的jvm设置的-Xmx100m 那个数据,由于lettuce底层的bug,它不能及时回收,所以不报关你设置多大,压测时都会报OutOfDirectMemoryError
原因分析:
//1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用通信
//2)、lettuce的bug导致netty堆外内存溢出 可设置:-Dio.netty.maxDirectMemory ,但是在高并发下还是无法解决问题,因为它并不会及时的释放连接,
**解决方案:**不能直接使用-Dio.netty.maxDirectMemory去调大堆外内存
//1)、升级lettuce客户端。 2)、切换使用jedis
当前lettuce的版本是
两种方案的比较:
lettuce 和 jedis 时底层对redis进行操作的客户端,而spring boot中redistemplete时在再一次封装了lettuce 和jedis 。
升级lettuce客户端 lettuce使用的netty作为服务器,吞吐量会更高,性能更好
jedis 客户端好久没有更新了。
当前我们的解决方案是切换使用jedis ,等以后再用升级lettuce客户端的方式取实现。
3.3 切换使用jedis操作redis 以避免压测时的OutOfDirectMemoryError
第一步:再pom中引入jedis并
<!-- 引入jredis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
第二步:在pom中排除io.lettuce
<!-- 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>
四、缓存失效问题
4.1 缓存穿透-查询数据库中没有的数据
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次
请求都要到存储层去查询,失去了缓存的意义。要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。在流量大时,可能 DB 就挂掉了。
解决:
场景一:缓存空结果、并且设置短的过期时间。
4.2 缓存雪崩-缓存大面积失效
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失
效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
例如:我们有100万的商品数据,设置了相同的过期时间,时间到了以后这100万的商品没有缓存了,此时突然有100万的并发过来,正好是请求这100万个商品的,就会出现缓存大面积失效而直接请求数据库的情况。
解决:
原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的
重复率就会降低,就很难引发集体失效的事件。
4.3 缓存击穿-某一个key生效,但是这个key正好是一个热点key
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,
是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所
有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
解决:
加锁
五、单体应用解决缓存失效问题
首先我们使用本地锁(synchronized或者JUC中的lock)解决,这种方案适用于单体应用。
本地锁没有把加入缓存的方法放在锁中导致的错误示例
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
Map<String, List<Catelog2Vo>> result = null;
// 1、加入缓存
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if(StringUtils.isEmpty(catelogJSON)){
// 如果缓存中有就用缓存的,如果没有就从数据库查,查出后把数据放入缓存
result = getCatalogJsonFromDB();
// 缓存中一般都时存放json,因为它可以跨语言,跨数据
if(result==null){
// 1.防止缓存穿透 添加空数据设置过期时间
stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString("{}"),60, TimeUnit.SECONDS);
}else{
// 2.防止缓存雪崩 添加随机过期时间
stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(result),new Random(5).nextLong(),TimeUnit.MINUTES);
}
}else{
result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
}
return result;
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
System.out.println("查询了数据库");
synchronized (this){
// 得到锁以后,我们应该去缓存中再确认一次,如果没有才需要继续查询
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if(!StringUtils.isEmpty(catelogJSON)){
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
//将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//1、查出所有分类
//1、1)查出所有一级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//封装数据
Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(category3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parentCid;
}
}
★本地锁的正确写法
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
Map<String, List<Catelog2Vo>> result = null;
// 1、加入缓存
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if(StringUtils.isEmpty(catelogJSON)){
// 如果缓存中有就用缓存的,如果没有就从数据库查,查出后把数据放入缓存
result = getCatalogJsonFromDB();
}else{
result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
}
return result;
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
System.out.println("查询了数据库");
synchronized (this){
// 得到锁以后,我们应该去缓存中再确认一次,如果没有才需要继续查询
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if(!StringUtils.isEmpty(catelogJSON)){
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
//将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//1、查出所有分类
//1、1)查出所有一级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//封装数据
Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(category3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
// 缓存中一般都时存放json,因为它可以跨语言,跨数据
if(parentCid==null){
// 1.防止缓存穿透 添加空数据设置过期时间
stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString("{}"),60, TimeUnit.SECONDS);
}else{
// 2.防止缓存雪崩 添加随机过期时间
stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(parentCid),(new Random(1).nextInt(5)+1),TimeUnit.MINUTES);
}
return parentCid;
}
}
★六、 分布式锁解决缓存失效的问题。
为什么要用分布式锁?
在分布式应用中,我们的某一个服务不是部署一份,而是部署多份在不同的服务器中的,本地锁只能锁住它自己的服务的代码,而无法所锁主其他微服务中的代码。
6.1 分布式锁的原理
6.2 使用redis的占锁功能来实现分布式锁的底层代码
redis底层方法
SET key value [EX seconds] [PX milliseconds] [NX|XX]
参数:
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
★NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值
127.0.0.1:6379> set lock hahaha NX
OK
127.0.0.1:6379> set lock hahaha NX
(nil)
6.3 分布式锁的错误写法:容易造成死锁
/**
* 使用redis实现分布式锁 - 错误写法啊
* 容易造成死锁现象
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLockError() {
//1、占分布式锁。去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
//加锁成功...执行业务
dataFromDb = getDataFromDb();
//删除我自己的锁
stringRedisTemplate.delete("lock");
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
// 调用自己重新尝试
return getCatalogJsonFromDBWithRedisLockError();
}
}
6.4 分布式锁的正确写法:避免死锁
修改步骤:
- 为每个请求设置不同的锁值,uuid
- 为每个锁设置过期时间,防止死锁
- 设置过期时间和添加锁时原子性的
- 捕捉业务中的异常,在finally中删除锁
- 删除锁代码是原子性的。
/**
* 使用redis实现分布式锁进行数据库查询
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonFromDBWithRedisLock();
// return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
原理图:
★七、分布式锁redission的使用
分布式锁与juc中的所有锁用法都是一样的。
上面的6.4 虽然能够为我们解决分布式锁的问题,但是官方已经不推荐我们这样使用,redission完全可以解决分布式锁的问题的,而且它功能强大,以后所有的分布式我们都会用这个工具去完成。是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)
redission 底层也是调用的redis的方法。
redis中文文档:http://www.redis.cn/
redission官网:https://github.com/redisson/redisson
redission中文文档:https://github.com/redisson/redisson/wiki/Table-of-Content
7.1 正式微服务整合redission
https://mvnrepository.com/search?q=Redisson
第一步:导入依赖
<!-- 以后使用Redisson作为所有分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
第二步:配置redisson,此处我们用的是程序化配置
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
// 使用单节点的redis
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
// rediss 安全连接
// config.useSingleServer().setAddress("rediss://192.168.56.10:6379");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
第三步:测试
@Autowired
RedissonClient redissonClient;
@Test
public void redisson(){
System.out.println("maruis------>" + redissonClient);
}
可以拿到客户端,所以整合成功
7.2 分布式锁最佳实战
@Autowired
private RedissonClient redisson;
@ResponseBody
@GetMapping(value = "/helloredisson")
public String helloRedisson() {
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
//myLock.lock(); //阻塞式等待。默认加的锁都是30s
//1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
myLock.lock(30,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
//问题:在锁时间到了以后,不会自动续期
//1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
//2、如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间30】 / 3 = 10s
// 3、最佳实战
// 实战中我们还是应该给锁加上过期时间,可以把锁的时间加大一点如30s,虽然这样有死锁的风险,但是如果一个业务30s还执行不完本身就有问题的
// 加上过期时间是为了不让看门狗自动续期,因为这样会降低程序的部分性能
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁,答案是不会因为锁默认有过期时间30s
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
7.3 分布式锁之—读写锁
读锁,和写锁一定是成对出现的
写锁控制了读锁,写锁存在,读写就都在等待。
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待
* @return
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
注意事项
测试结果:
写操作
读操作
7.4 分布式锁之—闭锁 人走完关门
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门
* 分布式闭锁
*/
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "放假了...";
}
@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}
测试:
7.4 分布式锁之—信号量锁,抢停车位
/**
* 车库停车
* 3车位
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号、获取一个值,占一个车位
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
7.5 分布式锁之—信号量锁做限流
/**
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park1")
@ResponseBody
public String park1() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
// 查实获取锁,无法获取就返回false
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
System.out.println("maruis------>" + "业务代码。。。。");
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go1")
@ResponseBody
public String go1() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
九、分布式锁-缓存一致性解决方案
/**
* 缓存里的数据如何和数据库的数据保持一致??
* 缓存数据一致性
* 1)、双写模式
* 2)、失效模式
* @return
*/
@Autowired
private RedissonClient redissonClient;
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//1、占分布式锁。去redis占坑
//(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
//RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
//创建读锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
RLock rLock = readWriteLock.readLock();
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
rLock.lock();
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
rLock.unlock();
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
}
9.1 分布式锁-缓存一致性解决方案 双写模式存在的问题
原理,更新数据库的时候同时更新缓存
9.2 分布式锁-缓存一致性解决方案 失效模式存在的问题
原理,更新数据库的时候删除缓存,下次请求的时候就会从数据库获取
9.3 原因分析
其实这两种方案都会导致数据不一致性的问题;比如在双写模式下,两个写的请求先后打过来,处理后,在写缓存是由于网络延迟等原因导致后写的请求先写缓存,先写的请求后写入缓存,这就导致了数据不一致性,缓存中的数据不是最新的数据;再比如在失效模式下,看图可知道,当我在第二个写请求还没完成时,我去读缓存,没有读到,然后去数据库中查,当我读到之后假设第二请求还没完成,当第二个请求完成之后,删掉缓存,我再更新到缓存中,也会导致数据不一致性的问题。
针对上面的问题,我们怎么解决呢?
9.4 解决方案
如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小的,就不用考虑数据不一致的问题,缓存数据加上过期时间,每隔一段时间触发读主动更新即可
如果是菜单、商品介绍等基础数据,也可以采用canal订阅binlog的方式,数据库中信息改变,canal采集这些信息,再做些处理然后同步到redis当中即可
缓存数据+过期时间足够解决大部分业务对于缓存的要求
如果写入操作稍多的话,我们可以通过加锁的方式去保证并发读写,写写的时候排好队,保证顺序,读的时候不加锁,所以适用读写锁(业务不关心脏数据,允许临时脏数据可忽略)。
对于我们能够放入缓存的数据就不应该是实时性、数据一致性要求高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新的数据即可。我们不应该过度的设计,增加系统的复杂性,遇到那些实时性、一致性要求高的数据,就应该去查询数据库,慢点就慢点。
cananl是一个中间件,它可以监听mysql生成的而二进制之日,当数据库更新后去更新redis中的数据。
9.5 最佳实战
为了保证我们系统数据一致性,我们要做如下的操作
- 缓存的所有数据都有过期时间,数据过期下一次查询出发主动更新
- 读写锁的使用,加上分布式的读写锁,经常写,经常读。
十一、SpringCash做缓存
11.1 文档
官网:https://docs.spring.io/spring-framework/docs/current/reference/html/
11.2 简介
** Spring 从 3.1 开始定义了 org.springframework.cache.Cache**
和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术;CacheManager 可以使用不同的工具来管理缓存,如可以使用redis也可以使用其他。
并支持使用 JCache(JSR-107)注解简化我们开发;
Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合;
Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已
经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓
存结果后返回给用户。下次调用直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
11.3 整合SpringCach+redis 做缓存
第一步:添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
// @Autowired
// public CacheProperties cacheProperties;
/**
* 配置文件的配置没有用上
* @return
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config = config.entryTtl(); 将存入缓存的数据转为json再存入
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
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;
}
}
第二步:添加配置
#使用redis作为缓存
spring.cache.type=redis
#spring.cache.cache-names=qq,毫秒为单位
spring.cache.redis.time-to-live=3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
自定义配置类,实现往redis中存json的要求
第三步:测试
使用@EnableCaching //开启缓存功能
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@RefreshScope
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.atguigu.gulimall.product.dao")
@EnableCaching //开启缓存功能
public class GulimallProductApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(GulimallProductApplication.class, args);
Map<String,Object>beans = run.getBeansOfType(Object.class);
}
}
**@Cacheable注解添加缓存**
/**
* 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
* 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
* 默认行为
* 如果缓存中有,方法不再调用
* key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
* 缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
* 默认时间是 -1:
*
* 自定义操作:key的生成
* 指定生成缓存的key:key属性指定,接收一个Spel
* 指定缓存的数据的存活时间:配置文档中修改存活时间
* 将数据保存为json格式
*
*
* 4、Spring-Cache的不足之处:
* 1)、读模式
* 缓存穿透:查询一个null数据。解决方案:缓存空数据
* 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
* 2)、写模式:(缓存与数据库一致)
* 1)、读写加锁。
* 2)、引入Canal,感知到MySQL的更新去更新Redis
* 3)、读多写多,直接去数据库查询就行
*
* 总结:
* 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
* 特殊数据:特殊设计
*
* 原理:
* CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
* @return
*/
// @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
@Cacheable(value = {"category"}) // 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用,没有就会调用这个方法
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys........");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
return categoryEntities;
}
十二、缓存的操作注解:
// @Cacheable #触发将数据保存到缓存的曹祖
// @CacheEvict # 删除缓存
// @CachePut # 更新缓存
// @Caching # 组合以上操作
// @CacheConfig # 在类级别共享栈
- 添加缓存
@Cacheable(value = {“category”},key = “#root.method.name”,sync = true)
/**
* 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
* 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
* 默认行为
* 如果缓存中有,方法不再调用
* key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
* 缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
* 默认时间是 -1:
*
* 自定义操作:key的生成
* 指定生成缓存的key:key属性指定,接收一个Spel
* 指定缓存的数据的存活时间:配置文档中修改存活时间
* 将数据保存为json格式
*
*
* 4、Spring-Cache的不足之处:
* 1)、读模式
* 缓存穿透:查询一个null数据。解决方案:缓存空数据
* 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
* 2)、写模式:(缓存与数据库一致)
* 1)、读写加锁。
* 2)、引入Canal,感知到MySQL的更新去更新Redis
* 3)、读多写多,直接去数据库查询就行
*
* 总结:
* 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
* 特殊数据:特殊设计
*
* 原理:
* CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
* @return
*/
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)
// @Cacheable(value = {"category"}) // 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用,没有就会调用这个方法
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys........");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
return categoryEntities;
}
- 删除缓存 用于实现删除模式
@CacheEvict(value = {“category”},key = “#root.method.name”)
// 事务注解,由于此处更新的时两张表,事务的开启实在config中的MybitsConfig配置的
@CacheEvict(value = {"category"},key = "#root.method.name")
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
baseMapper.updateById(category);
// 级联更新关系表中的冗余数据categoryname
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
删除category 这个分区下所有的注解 allEntries=true 所谓的分区再redis下其实并没有分区,只是再redis的内部给我们做的逻辑上的分区。
@CacheEvict(value = “category”,allEntries=true)
3. 组合执行
@Caching
注意:key中的值要再加一个单引号,否则缓存中的名称可能不对
@Caching(cacheable = {@Cacheable(value = "category",key = "'aaa'"),@Cacheable(value = "category",key = "'bbb'")},put={},evict = {})
- @CachePut 更新缓存,用户实现双写模式
@CachePut
@CachePut(value = "category",key = "'aaa'")
十三、Spring-Cach缓存的不足:
* 4、Spring-Cache的不足之处:
* 1)、读模式
* 缓存穿透:查询一个null数据。解决方案:缓存空数据
* 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题,此处加的时本地锁,一般加了本地锁就可以了。
* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
* 2)、写模式:(缓存与数据库一致)
* 1)、读写加锁。
* 2)、引入Canal,感知到MySQL的更新去更新Redis
* 3)、读多写多,直接去数据库查询就行
*
总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计
十三、缓存配置源码分析
- 配置类:CacheAutoConfiguration
- 导入的缓存的配置类:CacheConfigurationImportSelector
- 导入配置类的方法:CacheConfigurations.getConfigurationClass(types[i]);
- getConfigurationClass 中可选的缓存工具:
static {
Map<CacheType, Class<?>> mappings = new EnumMap(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
- RedisCacheConfiguration.class 我们选择的是redis
- public RedisCacheManager cacheManager 为我们提供cachemanager
- 并且determineConfiguration 中可以看到如果我们有自己的配置就用自己的没有就用默认的
ate org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader) {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
- RedisCacheConfiguration 配置类
private final Duration ttl;
private final boolean cacheNullValues;
private final CacheKeyPrefix keyPrefix;
private final boolean usePrefix;
private final SerializationPair<String> keySerializationPair;
private final SerializationPair<Object> valueSerializationPair;
private final ConversionService conversionService;
- RedisCacheConfiguration 配置类
八、注意事项
所有的分布式锁的用法跟juc中的锁用法是相同
锁是通过锁名来判断是不是同一把锁的,锁的粒度要尽可能小。