Redis读写锁和分布式锁的项目实践

笔记哥 / 05-20 / 19点赞 / 0评论 / 875阅读
## 问题1:在修改分组时,有短链接正在访问会出现什么问题?怎么解决 - 假设:现有线程A正在修改短链a的分组gid1为gid2(还未修改成功) - 同时有一个线程B获取了短链a分组gid1,要进行统计pv,uv,uip操作时.发现gid1已经不存在了,就会发生并发问题 ## 解决方案:采用读写锁 ### 什么是读写锁 > > > Redisson读写锁是一种基于Redis实现特殊的机制,用于在分布式系统中协调对共享资源的访问,其继承了Java中的`ReentrantReadWriteLock`的思想.特别适用于读多写少的场景.**其核心是:允许多个线程同时读取共享资源,但写操作必须占用资源**.从而保证线程安全的同时提高并发性能 > - 十分适合短链更新时:当某个线程需要更新资源时→需要获取写锁.此时所有的读操作和其他写线程会被阻塞,保证数据的一致性 # 核心原理 ### 读锁(共享锁): - 共享性:允许多个线程同时持有读锁 - 互斥性:只要存在读锁,该线程就不能获取写锁 ### 写锁(排他锁): - 独占性:同一时刻只能占有一个线程持有写锁 - 互斥性:当一个线程获取了写锁,其他线程就无法同时获取写锁和读锁.写锁占用线程修改共享资源,确保了在修改时没有其他线程访问 ### 基本代码结构如下: ```java RReadWriteLock configLock = redisson.getReadWriteLock("configLock"); // 读配置 configLock.readLock().lock(); try { return loadConfigFromCache(); } finally { configLock.readLock().unlock(); } // 写配置 configLock.writeLock().lock(); try { updateConfigInDB(); refreshCache(); } finally { configLock.writeLock().unlock(); } ``` # 分布式锁Redisson - 为防止缓存击穿→大量并发请求同时查询一个失效的缓存,导致数据库压力骤增 - 通过 Redisson 的 `RLock`(分布式锁)确保同一时刻只有一个线程执行数据库查询操作 ## 双重检查锁→重建缓存 ### **双重检查流程** 1. **第一次检查-无锁**: - **未在代码中显式体现**:通常在实际业务中,外层会先尝试从缓存读取数据。如果缓存命中,直接返回数据,无需加锁。此处代码直接处理未命中场景,可能外层已进行第一次检查。 - **假设场景**:当缓存未命中时,请求进入加锁逻辑。 2. **加锁后第二次检查-关键**: - 在获取分布式锁后,**再次检查缓存-**`stringRedisTemplate.opsForValue().get`。 - **目的:**确保在等待锁的过程中,其他线程可能已经更新了缓存,避免重复查询数据库。 ## 缓存穿透和缓存击穿解决方法 - 流程图如下: ![](https://cdn.res.knowhub.vip/c/2505/20/f7699890.png?G1YAAMTW3Dgp8EBW22gDdWfqnTYDElkElRLW6917rpvo%2bwMMy0%2bvbcT68JfaRpAaVOUgMJwdyYNFXZStnMncil3syLMH) - 布隆过滤器+缓存空值+redisson锁 - 缓存空值的操作对布隆过滤器误判操作进行保护→防止穿透 - redisson锁操作对大量空值同时过期操作进行保护→防止击穿 ```java RLock lock = redissonClient.getLock(String.format(LOCK_SHORT_LINK_GOTO_KEY, fullShortUrl)); // 加锁 lock.lock(); try { // 再次查询缓存 String originalUrl = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)); // 如果缓存中不存在,则查询数据库 if (StrUtil.isBlank(originalUrl)) { //查询goto表 LambdaQueryWrapper linkGotoQueryWrapperqueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapperqueryWrapper); if (shortLinkGotoDO == null) { //进行风控解决缓存穿透问题->设为空值 stringRedisTemplate.opsForValue().set(String.format(SHORT_LINK_IS_NULL_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); resp.sendRedirect("/page/notfound"); return; } //查询短链接表获取originUrl LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getEnableStatus, 0); ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper); //数据库中不存在或者短链接已经过期则跳转404页面 if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) { //短链接已经过期 stringRedisTemplate.opsForValue().set(String.format(SHORT_LINK_IS_NULL_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); resp.sendRedirect("/page/notfound"); return; } // 缓存原始链接 stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()); //预热缓存 stringRedisTemplate.opsForValue().set( String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl(), LinkUtil.getShortLintCacheValidDate(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS); // 访问统计 shortLinkAccessStats(fullShortUrl, shortLinkDO.getGid(), req, resp); // 跳转 resp.sendRedirect(shortLinkDO.getOriginUrl()); } } finally { lock. Unlock(); ```