问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

用互斥锁解决缓存击穿问题

创作时间:
作者:
@小白创作中心

用互斥锁解决缓存击穿问题

引用
CSDN
1.
https://blog.csdn.net/hguhbh/article/details/139521951

缓存击穿是高并发系统中常见的问题之一,它通常发生在热点数据的缓存失效时,导致大量请求直接打到数据库,造成数据库压力剧增。本文将详细介绍如何使用互斥锁(Mutex)来解决缓存击穿问题,并通过实际代码和性能测试验证解决方案的有效性。

在高并发场景下,一个被频繁访问的缓存数据(热点key)突然失效,如果没有有效的防护措施,大量请求会直接冲击数据库,导致数据库负载激增,甚至引发系统崩溃。为了解决这一问题,本文将介绍使用互斥锁(Mutex)的解决方案。

缓存击穿问题概述

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务复杂的存储在redis中的key突然失效,无数请求就会瞬间打到数据库造成巨大冲击。

解决方案:互斥锁

互斥锁是一种常用的解决方案,其核心思想是:当一个线程获取到互斥锁时,其他线程必须等待,直到锁被释放。这样可以确保在缓存重建期间,只有一个线程负责查询数据库并更新缓存,其他线程则等待或返回旧数据。

代码实现

我们使用Redis的setnx命令来实现互斥锁,该命令只有在key不存在时才会创建成功,若key已存在就会创建失败。

private boolean tryLock(String key) {
    //参数分别是,key,value,过期时间,过期时间的单位
    //这里过期时间用事先写的静态变量,10L
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag); //如果直接返回flag,当flag为null时,会做拆箱,报错空指针。
}

private void UnLock(String key) {
    stringRedisTemplate.delete(key);
}

接下来,我们在服务层实现具体的缓存重建逻辑:

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public Result queryById(Long id) {
        //用互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    public Shop queryWithMutex(Long id) {
        //1.从redis查询数据缓存
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) { //isNotBlank方法只有有值字符串才会返回true,null和空值都会返回false
            //3.存在,返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //shopJson不存在
        //判断查到的数据是否为空值(这个空值指的不是null,是空字符串)
        if (shopJson != null) {
            //返回错误信息
            return null;
        }
        //4实现缓存重建
        //4.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean lock = tryLock(lockKey);
        //4.2判断是否获取成功
        Shop shop = null;
        try {
            if (!lock) {
                //4.3失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.4成功,根据id查询数据库
            shop = getById(id);
            //模拟数据库重建的延时
            Thread.sleep(200);
            //5.不存在,返回错误
            if (shop == null) {
                //将空值缓存到redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //6.存在,写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            UnLock(lockKey);
        }
        //8.返回
        return shop;
    }
}

性能测试

为了验证解决方案的有效性,我们使用JMeter进行压力测试。开启100个线程进行测试,结果所有请求都成功了。通过查看控制台日志,可以发现只查询了一次数据库,这说明互斥锁机制成功地防止了缓存击穿。

总结

通过使用互斥锁,我们可以有效地解决缓存击穿问题。虽然这种方法可能会带来一定的性能开销,但在高并发场景下,它能够显著降低数据库的压力,保障系统的稳定运行。在实际应用中,还可以结合其他策略(如逻辑过期时间)来进一步优化解决方案。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号