1. 缓存使用
哪些数据适合放入缓存?
即时性、数据一致性要求不高的
访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率
来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
2. 在获取3级目录列表接口中使用缓存
@GetMapping(value = "/index/catalog.json") @ResponseBody public Map<String, List<Catelog2Vo>> getCatalogJson() { Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson(); return catalogJson; }
@Override public Map<String, List<Catelog2Vo>> getCatalogJson() { String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (StringUtils.isEmpty(catalogJson)){ Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); String s = JSON.toJSONString(catalogJsonFromDb); stringRedisTemplate.opsForValue().set("catalogJson",s); return catalogJsonFromDb; } System.out.println("从缓存中获取了数据!!!!"); Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() { System.out.println("查询了数据库"); //将数据库的多次查询变为一次 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; }
访问接口第一次是从数据款中获取数据,第二次就会从缓存中直接获取数据
在使用缓存过程中遇到的问题
在并发场景下会产生对外内存溢出:OutOfDirectMemoryError:
(1)springboot2.0以后默认使用lettuce操作redis的客户端,它使用netty进行网络通信
(2)lettuce的bug导致netty堆外内存溢出 可设置:-Dio.netty.maxDirectMemory
(3)解决方案:不能直接使用-Dio.netty.maxDirectMemory去调大堆外内存
(4)1)、升级lettuce客户端。 2)、切换使用jedis
3. 缓存失效问题
(1)缓存穿透
(2)缓存雪崩
(3)缓存击穿
4. 使用本地锁解决缓存穿透问题
由于springboot所有的组件都是单例的,即使有批量请求也是使用同一把锁,所以可以使用synchronized (this)来加锁,第一个请求来时获取锁,查询数据库,在查询之前再次确认下缓存中是否有数据,如果没有则从数据库中查询,获取到数据后存入缓存中。
修改方法实现
@Override public Map<String, List<Catelog2Vo>> getCatalogJson() { /** * 使用缓存过程中存在的问题 * 1. 空结果缓存:解决缓存穿透问题 * 2. 设置过期时间(加随机值):解决缓存雪崩 * 3. 加锁:解决缓存击穿问题 */ String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (StringUtils.isEmpty(catalogJson)){ //2. 缓存中没有数据,查询数据款 System.out.println("缓存不命中...查询数据库"); Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithLocalLock(); return catalogJsonFromDb; } System.out.println("缓存命中...直接返回"); Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() { //只要是同一把锁就可以锁住所有的线程 //1. synchronized (this): springboot所有的组件都是单例的,即使有批量请求也是使用同一把锁 synchronized (this){ return getCatalogJsonFromDb(); } } public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() { //得到锁以后,应该先去缓存中确定一次,如果没有再进行查询 String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (!StringUtils.isEmpty(catalogJson)){ //缓存不为空直接返回 Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } System.out.println("查询了数据库"); //将数据库的多次查询变为一次 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 String s = JSON.toJSONString(parentCid); stringRedisTemplate.opsForValue().set("catalogJson",s,1, TimeUnit.DAYS); return parentCid; }
在这种场景下需要注意的是:一定要在同步锁的执行过程中,将从数据库中查询的数据放入缓存中,否则,如果在同步锁执行完成后在存入缓存就会出现查询两次数据库的可能,因为在第一次查询数据库后,会释放锁,然后存入缓存的过程中,这是第二个请求获取锁查询数据此时可能第一个请求的存放还没有完成,这是第二个请求来获取数据还不能从缓存中获取数据,还是得查询一次数据库,所以存入缓存的操作应该放在同步锁内执行。
使用jmeter批量请求测试时,第一次查询了两次数据库
当把存放缓存的操作放在同步锁内时,只会查询一次数据库
由本地锁解决缓存击穿所带来的问题:
在单体应用的场景下,由于springboot是单实例的,可以通过共用一个同步锁来解决批量请求下缓存击穿的问题。
但是在分布式的场景下,会存在多个容器,多个实例,每一个this只能代表一个锁,所以每一个实例上都会存在通过this来锁住10000个请求,让其中的一个请求获取锁查询数据库,就会有8个线程同时访问数据库查询相同的数据,所以本地锁只能锁住当前进程,在分布式的场景下就需要分布式锁。
启动多个商品服务,使用jemeter测试,每一个服务都会去查询一次数据库,说明在分布式场景下,使用本地锁并不能完全锁住所有的请求。
5. 使用分布式锁解决缓存穿透问题
基本流程:可以同时去一个地方设置值,当可以设置成功时,此时就是获取锁,就去执行逻辑,否则就必须等待,直到释放锁,等待可以使用自旋的方式。设置值可以使用redis的set值,当值不存在才去set值,并设置缓存时间。基本命令 NX 表示不存在
127.0.0.1:6379> set hello world NX OK 127.0.0.1:6379> set hello world NX (nil)
(1)第一阶段实现
实现逻辑
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1. 占分布式锁 Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111"); if (lock){ //加锁成功...执行业务 Map<String, List<Catelog2Vo>> jsonFromDb = getCatalogJsonFromDb(); stringRedisTemplate.delete("lock"); //必须要删除锁,其他人才可以获取锁 return jsonFromDb; }else { //加锁失败...重试 //休眠一段时间 return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
存在问题:
当获取到业务数据后,如果在删除锁的过程中发生了异常,就会存在没有删除锁的可能,这样其他人就不能获取到锁,这样就造成死锁,所以在设置锁的同时,要同时设置过期时间。
(2)第二阶段实现
实现逻辑
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1. 占分布式锁 Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111"); if (lock){ //加锁成功...执行业务
// 发生异常
//2. 设置过期时间 stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS); Map<String, List<Catelog2Vo>> jsonFromDb = getCatalogJsonFromDb(); stringRedisTemplate.delete("lock"); //必须要删除锁,其他人才可以获取锁 return jsonFromDb; }else { //加锁失败...重试 //休眠一段时间 return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
存在问题
在获取到锁之后,还没有设置过期时间时,发生异常,此时又会发生死锁的可能。所以设置过期时间和占锁必须是原子性的,可以使用redis setnx ex命令
127.0.0.1:6379> set hello world EX 300 NX OK 127.0.0.1:6379> TTL hello (integer) 293
(3)第三阶段实现
实现逻辑
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1. 占分布式锁,同时设置过期时间 Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",300,TimeUnit.SECONDS); if (lock){ //加锁成功...执行业务 Map<String, List<Catelog2Vo>> jsonFromDb = getCatalogJsonFromDb(); stringRedisTemplate.delete("lock"); //必须要删除锁,其他人才可以获取锁 return jsonFromDb; }else { //加锁失败...重试 //休眠一段时间 return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
存在问题
由于业务执行耗时较长,在还没有删除时已经过期了,就会存在当第二个请求过来时,会把第二个请求还在使用的锁删除了。所以在删除锁时需要执行uuid,只能删除自己所持有的锁。
(4)第四阶段
实现逻辑
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1. 占分布式锁,同时设置过期时间 String uuid = UUID.randomUUID().toString(); Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS); if (lock){ //加锁成功...执行业务 Map<String, List<Catelog2Vo>> jsonFromDb = getCatalogJsonFromDb(); String lockVaule = stringRedisTemplate.opsForValue().get("lock"); if (uuid.equals(lockVaule)){ //删除自己的锁 stringRedisTemplate.delete("lock"); //必须要删除锁,其他人才可以获取锁 } return jsonFromDb; }else { //加锁失败...重试 //休眠一段时间 return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
存在问题:
在获取到lockValue值后,并且判断了uuid相等于lockValue后,由于网络传输过程中的耗时,此时redis中保存的lock值刚好到期了,lock已经被别人所占有,那么就会把别人的锁删除了。所以需要保证删除锁和获取值的操作也应该时原子性的,使用redis+lua脚本实现。
(5)最终阶段,在加锁和解锁都需保证原子性
实现逻辑
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1. 占分布式锁,同时设置过期时间 String uuid = UUID.randomUUID().toString(); Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS); if (lock){ //加锁成功...执行业务 System.out.println("获取分布式锁成功..."); Map<String, List<Catelog2Vo>> jsonFromDb = null; try{ jsonFromDb = getCatalogJsonFromDb(); }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); } //获取值对比和删除=原子操作,lua脚本 // String lockVaule = stringRedisTemplate.opsForValue().get("lock"); // if (uuid.equals(lockVaule)){ // //删除自己的锁 // stringRedisTemplate.delete("lock"); //必须要删除锁,其他人才可以获取锁 // } return jsonFromDb; }else { System.out.println("获取分布式锁失败...等待重试..."); //加锁失败...重试 //休眠一段时间 try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
使用redis作为分布式锁解决缓存击穿问题完整实现
@GetMapping(value = "/index/catalog.json") @ResponseBody public Map<String, List<Catelog2Vo>> getCatalogJson() { Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson(); return catalogJson; } @Override public Map<String, List<Catelog2Vo>> getCatalogJson() { /** * 使用缓存过程中存在的问题 * 1. 空结果缓存:解决缓存穿透问题 * 2. 设置过期时间(加随机值):解决缓存雪崩 * 3. 加锁:解决缓存击穿问题 */ String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (StringUtils.isEmpty(catalogJson)){ //2. 缓存中没有数据,查询数据款 System.out.println("缓存不命中...查询数据库"); Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock(); return catalogJsonFromDb; } System.out.println("缓存命中...直接返回"); Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1. 占分布式锁,同时设置过期时间 String uuid = UUID.randomUUID().toString(); Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS); if (lock){ //加锁成功...执行业务 System.out.println("获取分布式锁成功..."); Map<String, List<Catelog2Vo>> jsonFromDb = null; try{ jsonFromDb = getCatalogJsonFromDb(); }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); } //获取值对比和删除=原子操作,lua脚本 // String lockVaule = stringRedisTemplate.opsForValue().get("lock"); // if (uuid.equals(lockVaule)){ // //删除自己的锁 // stringRedisTemplate.delete("lock"); //必须要删除锁,其他人才可以获取锁 // } return jsonFromDb; }else { System.out.println("获取分布式锁失败...等待重试..."); //加锁失败...重试 //休眠一段时间 try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDbWithRedisLock(); //自旋 } } public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() { //得到锁以后,应该先去缓存中确定一次,如果没有再进行查询 String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (!StringUtils.isEmpty(catalogJson)){ //缓存不为空直接返回 Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } System.out.println("查询了数据库"); //将数据库的多次查询变为一次 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 String s = JSON.toJSONString(parentCid); stringRedisTemplate.opsForValue().set("catalogJson",s,1, TimeUnit.DAYS); return parentCid; }
使用jemeter测试500个线程发送请求,启动4个商品服务,最终只有一个服务中的一个请求会查询数据库操作,其他都不会查询数据库。
6. 使用Redisson作为分布式锁
(1)在pom中引入Redisson
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.12.0</version> </dependency>
(2)创建配置类
@Configuration public class MyRedissonConfig { @Bean(destroyMethod = "shutdown") public RedissonClient redisson() throws IOException { //1、创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://123.57.130.76:6379"); //2、根据Config创建出RedissonClient实例 //Redis url should start with redis:// or rediss:// RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
(3)使用
@ResponseBody @GetMapping(value = "/hello") public String hello() { //1. 获取锁 RLock lock = redisson.getLock("my-lock"); //2. 加锁 // lock.lock(); //阻塞等待 //(1)锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期被删除; //(2)加锁的业务只要运行完成,就不会给锁续期,即使不手动删除,锁也会在30s以后自动删除; lock.lock(10, TimeUnit.SECONDS); ////10秒钟自动解锁,自动解锁时间一定要大于业务执行时间 //问题:在锁时间到了以后,不会自动续期 //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间 //2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】 //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒 // internalLockLeaseTime 【看门狗时间】 / 3, 10s try { System.out.println("加锁成功,执行业务。。。" + Thread.currentThread().getId()); Thread.sleep(30000); }catch (Exception e){ }finally { System.out.println("释放锁。。。" + Thread.currentThread().getId()); lock.unlock(); } return "hello"; }
7. 使用redisson作为读写锁
@GetMapping(value = "/write") @ResponseBody public String writeValue() { RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); RLock rLock = readWriteLock.writeLock(); String s = ""; try { //1. 改数据加写锁,读数据加读锁 rLock.lock(); s = UUID.randomUUID().toString(); Thread.sleep(30000); redisTemplate.opsForValue().set("writeValue",s); }catch (Exception e){ e.printStackTrace(); }finally { rLock.unlock(); } return s; } @GetMapping(value = "/read") @ResponseBody public String readValue() { RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); RLock rLock = readWriteLock.readLock(); String s = ""; try { rLock.lock(); Thread.sleep(10000); s = redisTemplate.opsForValue().get("writeValue"); }catch (Exception e){ e.printStackTrace(); }finally { rLock.unlock(); } return s; }
先发送write请求,在获取到写锁后,在写操作时,读操作也处于等待中;当写操作完成释放锁后,获取到读锁,可以读到写入的数据;保证一定能读到最新的数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁,写锁没释放读锁就必须等待。
读 + 读 :相当于无锁,并发读,只会在redis中记录好所有的读锁,他们都会同时加锁;
写 + 读:等待写锁释放;
写 + 写:阻塞方式;
读 + 写:由读锁,写也需要等待;
只要有写的存在,都必须等待;
8. 使用redisson作为信号量
@GetMapping(value = "/park") @ResponseBody public String park() throws InterruptedException { RSemaphore park = redisson.getSemaphore("park"); // park.acquire(); //获取一个信号,获取一个值,相当于占有一个车位,如果没有则等待; boolean b = park.tryAcquire();//尝试获取,如果没有则返回false if (b){ //执行业务 }else { return "error"; } return "Ok" + b; } @GetMapping(value = "/go") @ResponseBody public String go() { RSemaphore park = redisson.getSemaphore("park"); park.release(); //释放一个车位 return "ok"; }
在redis中设置初始park为3,每执行一次park请求,则相当于占有一个车位,park值就会减1,减到0后,就会返回false,当每执行一次go请求,则相当于释放一个车位,park值就会加1,此时就可以再次执行park操作了。使用信号量也可以作为分布式限流操作。
9. 使用redisson作为闭锁
/** * 放假、锁门 * 1班没人了 * 5个班,全部走完,我们才可以锁大门 * 分布式闭锁 */ @GetMapping(value = "/lockDoor") @ResponseBody public String lockDoor() throws InterruptedException { RCountDownLatch door = redisson.getCountDownLatch("door"); door.trySetCount(3); 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 + "班的人都走了..."; }
当所有线程都执行完成后,再执行其他操作;
执行锁大门的操作的前提是班级的人都走完了,先执行lockDoor请求会处于等待状态,并且开始设置door的值是3,代表有3个班级,每走一个班级,则door的值减1,当door的值减为0后,代表所有的线程已经执行完成,就可以完成lockDoor操作了。
10. 使用redisson作为分布式锁修改查询3级分类商品列表
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() { //1. 占分布式锁,锁的粒度越细越快(具体缓存的是哪个商品,就对哪个商品加锁,而不是全部加锁) RLock lock = redissonClient.getLock("CatalogJson-lock"); lock.lock(); Map<String, List<Catelog2Vo>> dataFromDb = null; try { dataFromDb = getCatalogJsonFromDb(); }finally { lock.unlock(); } return dataFromDb; }
问题:缓存一致性问题
当修改了某一个3级商品的数据时,又会带来缓存的不一致性问题
保证缓存一致性的两种模式
双写模式当写入数据库后,同时写入缓存来保证一致性
但是这种模式也有其他问题:当请求1写入数据库后,还没来得及写入缓存,此时请求2写入了数据库同时立刻写入缓存,然后请求1才进行写入缓存操作,那么缓存的数据就会出现不一致情况;
失效模式:当写入数据库后,进行删除缓存操作,然后读取数据时,缓存中没有,就读取数据库,然后更新缓存;
但是失效模式也会带来其他问题:请求1先写入数据库后,然后删除了缓存;此时请求2也写入了数据库,还没来得及删除缓存时;请求3读取缓存时,没有读到数据,然后从数据库中读取数据,更新了缓存1;本来缓存中应该更新成缓存2的数据,现在又会更新成缓存1的数据。也会存在脏数据的可能。
但是这种脏数据也只是暂时的,最终还是可以更新成正确的数据,所有缓存虽然不能保证实时一致性,可以保证最终的一致性。
解决缓存一致性的两种方案:
(1)缓存设置过期时间,缓存过期后下一次查询主动更新;
(2)使用分布式读写锁,读数据操作等待写数据操作完成后查询;
(3)使用cananl中间件,mysql更新操作会记录到binlog日志中,cananl订阅binlog日志,当发现更新操作时,实时更新redis
11. 使用spring cache缓存
a. pom中引入spring-boot-starter-cache
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
b. 启动类上添加开启缓存功能的注解
@EnableCaching //开启缓存 @EnableFeignClients(basePackages = "com.lewang.gulimall.product.feign") @SpringBootApplication @MapperScan("com.lewang.gulimall.product.dao") @EnableDiscoveryClient public class GulimallProductApplication { public static void main(String[] args) { SpringApplication.run(GulimallProductApplication.class, args); } }
c. 配置类中指定使用redis
#application.properties spring.cache.type=redis #spring.cache.cache-names=qq,毫秒为单位 spring.cache.redis.time-to-live=3600000
(1)@Cacheable注解:把数据存入缓存
@GetMapping(value = {"/","index.html"}) private String indexPage(Model model) { //1、查出所有的一级分类 List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys(); model.addAttribute("categories",categoryEntities); return "index"; } @Override @Cacheable(value = {"category"},key = "'level1Categorys'") public List<CategoryEntity> getLevel1Categorys() { System.out.println("getLevel1Categorys........"); List<CategoryEntity> categoryEntities = this.baseMapper.selectList( new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }
在接口中查询一级分类数据后,使用注解的方式把数据存入缓存。
使用@Cacheable注解,value指定缓存的分区,key指定缓存的key,还可以指定缓存的过期时间,可以在配置文件中配置ttl属性。key值还支持表达式
key = "#root.method.name" 表示 缓存中的key值就是方法名
(2). SpringCache自定义缓存配置
直接使用注解的形式,在redis中保存的值是未经过序列化的value,可以自定义缓存配置序列化value
a. 缓存配置类
@EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // config = config.entryTtl(); 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; } }
b. 可以在配置文件中指定一些配置applicaiton.properties
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
c. 重新访问接口
缓存中保存的值已经是经过序列化后的值
并且如果接口的返回值是Null,也是可以保存到缓存中的,因为已经配置可以缓存空值,可以防止缓存穿透问题
(3)@CacheEvict 删除缓存
a. 要删除一个分区下的缓存可以直接使用
@CacheEvict(value = "category",key = "'getLevel1Categorys'")
b. 要删除一个分区下的多个缓存,有两种方式
/**
* @CacheEvict :失效模式,更新时删除缓存
*
* 1、同时进行多种缓存操作:@Caching
* 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
*
*/
/*@Caching(evict = {
@CacheEvict(value = {"category"},key = "'level1Categorys'"),
@CacheEvict(value = "category",key = "'getCatalogJson'")
})*/
@CacheEvict(value = "category",allEntries = true) //删除某个分区下的所有数据
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
(4)Spring-Cache的不足
a. 读模式
缓存穿透:查询一个null数据。解决方案:在配置文件中可以指定缓存空数据;
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间;
b. 写模式(缓存与数据库一致)
读写加锁;
引入Canal,感知到MySQL的更新去更新Redis;
读多写多,直接去数据库查询就行;
@Override
@Cacheable(value = {"category"},key = "'level1Categorys'",sync = true)
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys........");
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤
2021-08-08 java io流04 File
2021-08-08 java io流03 字符流