记录专辑详情缓存、Redisson 分布式锁、自定义缓存注解、布隆过滤器和 MySQL/Redis 一致性问题。
本篇要点
- 给专辑详情增加缓存
- 用 Redisson 控制热点回源
- 区分穿透、击穿和缓存一致性
**学习提示:**Redisson 在这里解决并发锁问题,不是分布式事务;布隆过滤器主要用于拦截不存在的键。
内容回顾
1、缓存问题
2、分布式锁
3、分布式锁四个条件
- 互斥性。
- 不会发生死锁。
- 解铃还须系铃人。
- 加锁和解锁必须具有原子性。
今天内容
1、专辑详情添加缓存
- 缓存添加 + 解决缓存击穿(分布式锁)
- setnx + 过期时间 + uuid + lua脚本实现
在专辑详情service方法里面添加

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @Override public AlbumInfo getAlbumInfo(Long albumId) { String albumKey = RedisConstant.ALBUM_INFO_PREFIX+albumId; AlbumInfo albumInfo = (AlbumInfo)redisTemplate.opsForValue().get(albumKey); if(albumInfo == null) { String lockKey = RedisConstant.ALBUM_INFO_PREFIX+albumId+ ":lock"; String uuid = UUID.randomUUID().toString(); Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 5, TimeUnit.SECONDS); if(ifAbsent) { try { AlbumInfo albumInfoData = this.getAlbumInfoData(albumId);
if (null == albumInfoData){ AlbumInfo albumInfo1 = new AlbumInfo(); redisTemplate.opsForValue().set(albumKey,albumInfo1,RedisConstant.ALBUM_TEMPORARY_TIMEOUT,TimeUnit.SECONDS); return albumInfo1; } redisTemplate.opsForValue().set(albumKey,albumInfoData, 10, TimeUnit.MINUTES); return albumInfoData; }finally { delRedisKey(lockKey, uuid); } } else { return getAlbumInfo(albumId); } } else { return albumInfo; } }
private void delRedisKey(String lockKey, String uuid) { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); 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"; redisScript.setScriptText(script); redisScript.setResultType(Long.class); redisTemplate.execute(redisScript, Arrays.asList(lockKey),uuid); }
|
- 上面代码虽然可以实现缓存+分布式锁功能,但是有两个缺点
第一个:代码太复杂了(使用Redisson减少复杂度)
第二个:业务代码和加锁解锁代码混合在一起(自定义注解+aop简化)
2、Redisson实现分布式锁
概述
第一个机制:重试机制
– 重试机制是指在分布式锁中,如果没有获取到锁,Redisson会自动进行重试,直到获取到锁或者超时。当尝试获取锁时,需要传入了时间参数!
第二个机制:看门狗机制(WatchDog,自动续期机制)
Redisson会启动一个守护线程来监控锁的情况,如果锁快要过期了,守护线程会自动续期。目的是保证操作在有锁状态下执行。
如果启动看门狗机制,首先不能传递过期时间,使用默认过期时间是30s
每个10s检查一次,当前锁是否被持有,如果被持有,对锁进行续期,续期到30s
底层都是通过 lua脚实现的
专辑详情使用Redisson分布式锁
第一步 引入Redisson依赖
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> </dependency>
|
第二步 初始化RedissonClient对象
- 在service-util模块创建配置类,初始化RedissonClient对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Data @Configuration @ConfigurationProperties("spring.data.redis") public class RedissonConfig { private String host;
private String password;
private String port;
private int timeout = 3000; private static String ADDRESS_PREFIX = "redis://";
@Bean RedissonClient redissonSingle() { Config config = new Config();
if(StringUtils.isEmpty(host)){ throw new RuntimeException("host is empty"); } SingleServerConfig serverConfig = config.useSingleServer() .setAddress(ADDRESS_PREFIX + this.host + ":" + port) .setTimeout(this.timeout); if(!StringUtils.isEmpty(this.password)) { serverConfig.setPassword(this.password); } return Redisson.create(config); } }
|
第三步 在专辑详情方法添加缓存+分布式锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @Autowired private RedissonClient redissonClient;
public AlbumInfo getAlbumInfoRedisson(Long albumId) throws Exception { String albumKey = RedisConstant.ALBUM_INFO_PREFIX+albumId; AlbumInfo albumInfo = (AlbumInfo)redisTemplate.opsForValue().get(albumKey);
if(albumInfo == null) { String lockKey = RedisConstant.ALBUM_INFO_PREFIX+albumId+ ":lock"; RLock rLock = redissonClient.getLock(lockKey); boolean tryLock = rLock.tryLock(3,5,TimeUnit.SECONDS); if(tryLock) { try { AlbumInfo albumInfoData = this.getAlbumInfoData(albumId);
redisTemplate.opsForValue().set(albumKey,albumInfoData, 10,TimeUnit.MINUTES); return albumInfoData; }finally { rLock.unlock(); }
} else { this.getAlbumInfoRedisson(albumId); } } else { return albumInfo; } return null; }
|
3、自定义注解+AOP专辑缓存
概述
专辑详情方法还原
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Override public AlbumInfo getAlbumInfo(Long albumId) { return this.getAlbumInfoData(albumId); }
private AlbumInfo getAlbumInfoData(Long albumId) { AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
if(albumInfo != null) { LambdaQueryWrapper<AlbumAttributeValue> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(AlbumAttributeValue::getAlbumId,albumId); List<AlbumAttributeValue> albumAttributeValueList = albumAttributeValueMapper.selectList(wrapper);
albumInfo.setAlbumAttributeValueVoList(albumAttributeValueList); } return albumInfo; }
|
创建自定义注解
1 2 3 4 5 6 7 8 9 10
| @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface GuiGuCache {
String prefix() default "cache"; }
|
创建切面类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @Aspect @Component public class GuiGuCacheAspect {
@Autowired private RedisTemplate redisTemplate;
@Autowired private RedissonClient redissonClient; @SneakyThrows @Around("@annotation(com.atguigu.tingshu.common.cache.GuiGuCache)") public Object cacheAspect(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); MethodSignature signature = (MethodSignature)joinPoint.getSignature(); Method method = signature.getMethod(); GuiGuCache guiGuCache = method.getAnnotation(GuiGuCache.class); String prefix = guiGuCache.prefix(); String key = prefix + Arrays.asList(args).toString();
Object obj = redisTemplate.opsForValue().get(key);
if (obj == null) { RLock rLock = redissonClient.getLock(key + ":lock"); boolean tryLock = rLock.tryLock(3, 5, TimeUnit.SECONDS); if(tryLock) { try { obj = joinPoint.proceed(args); if (null == obj){ Object o = new Object(); this.redisTemplate.opsForValue().set(key, o, 10, TimeUnit.MINUTES); return o; } redisTemplate.opsForValue().set(key, obj,10, TimeUnit.MINUTES); return obj; }finally { rLock.unlock(); }
} else { return cacheAspect(joinPoint); } } else { return obj; } } }
|
在专辑详情方法上添加注解
1 2 3 4 5 6 7
|
@GuiGuCache(prefix = "album:info:") @Override public AlbumInfo getAlbumInfo(Long albumId) { return this.getAlbumInfoData(albumId); }
|
4、布隆过滤器
概述
- 缓存穿透问题:查询数据,在缓存和mysql都不存在
- 解决方案一:把null添加缓存,没法防止随机穿透
- 解决方案二:使用布隆过滤器解决
布隆过滤器(Bloom Filter),是1970年,由一个叫布隆的小伙子提出的,距今已经五十年了。

第一步 使用n个映射函数计算数据在数组里面位置,n个映射函数计算出n个位置
第二步 把数组中计算出来的位置的值修改为1
第一步 使用n个映射函数计算数据在数组里面位置,n个映射函数计算出n个位置
第二步 查看计算出位置的值,如果有任何一个值是0肯定不存在的,如果位置中值都是1可能存在
专辑详情添加布隆过滤器

初始化布隆过滤器
- 一次性把所有专辑id都放到布隆过滤器里面
- 每次添加专辑之后,把专辑id放到布隆过滤器里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class ServiceAlbumApplication implements CommandLineRunner {
public static void main(String[] args) { SpringApplication.run(ServiceAlbumApplication.class, args); }
@Autowired private RedissonClient redissonClient; @Autowired private AlbumInfoService albumInfoService;
@Override public void run(String... args) throws Exception { RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER); bloomFilter.tryInit(100000,0.01); List<AlbumInfo> list = albumInfoService.list(); for (AlbumInfo albumInfo : list) { Long albumId = albumInfo.getId(); bloomFilter.add(albumId); } } }
|
在专辑详情方法里面添加布隆过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13
| @GetMapping("getAlbumInfo/{albumId}") public Result<AlbumInfo> getAlbumInfo(@PathVariable Long albumId) { RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER); if(!bloomFilter.contains(albumId)) { throw new GuiguException(228,"专辑不存在:"+albumId); } AlbumInfo albumInfo = albumInfoService.getAlbumInfo(albumId); return Result.ok(albumInfo); }
|
5、mysql 与 redis 数据一致性
概述
- 专辑数据添加redis缓存,如果修改mysql数据之后,同步redis不及时,可能造成mysql和redis数据不一致
- 登录之后用户信息添加redis里面,如果修改用户信息,同步redis不及时,可能造成mysql和redis数据不一致
延时双删策略
- 先删除缓存;
- 然后执行写数据库的操作;
- 休眠一段时间(例如500毫秒);
- 再次删除缓存。

- 使用以上策略不能保证数据实时一致性的,只能保证数据最终一致性
canal同步方案

- canal是阿里巴巴开发一款数据同步工具,相当于mysql主从复制中从机角色
- 机制:
– canal实时监控主机里面二进制日志变化,当二进制日志发生变化,canal实时获取变化的数据,把变化数据同步到redis里面