【redis实战篇】第三天

摘要:

        缓存优化是提升系统性能的关键策略,其核心在于减少数据库访问、降低延迟、减轻数据库负载并支持高并发。通过将数据存储在内存中,缓存能够提供更快的访问速度,从而显著提升应用程序性能。

        此外,缓存策略的优化,如热点操作逻辑的修改和缓存与数据库数据的同步,也是确保数据一致性和系统稳定性的重要手段。针对缓存穿透、缓存雪崩和缓存击穿等问题,文章提出了多种解决方案,包括缓存空值、布隆过滤器、互斥锁和逻辑过期等,以应对不同的缓存挑战,确保系统的高效运行。

一,缓存优化

1,缓存优化的好处:

  1. 提高性能:缓存可以显著减少数据库的访问次数,因为频繁的数据库访问会导致性能瓶颈。通过将数据存储在内存中,缓存可以提供更快的访问速度,从而提高应用程序的整体性能。

  2. 减少延迟:缓存数据通常存储在更接近用户的位置(如内存或本地磁盘),这比从远程数据库服务器获取数据要快得多。因此,缓存可以减少网络延迟,提高用户体验。

  3. 减轻数据库负载:缓存可以减少数据库的读写压力,因为许多请求可以直接从缓存中获取数据,而不需要访问数据库。这有助于防止数据库过载,并延长数据库的寿命。

  4. 支持高并发:缓存可以处理大量并发请求,因为它将数据存储在内存中,内存的访问速度远高于磁盘。因此,缓存可以支持高并发场景,而不会导致系统崩溃。

2,修改热点操作逻辑,如:查询店铺具体信息操作

逻辑:redis查询,命中返回;未命中,查询数据库,然后更新数据到缓存

    public Result queryShopInfoById(Long id) {
        //1,查询缓存
        String key = CACHE_SHOP_KEY+id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2,判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3,存在直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4,缓存不存在,查数据库
        Shop shop = getById(id);
        if (shop == null) {
            //5,不存在,返回错误
            return Result.fail("店铺不存在!");
        }
        //6,存在,写入缓存,并返回
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

二,缓存策略

为了解决缓存与数据库数据不一致的问题,我们需要对数据库和缓存中的数据进行同步,在执行更新或者修改操作时要修改数据库和缓存。

总结:

        首先开启事务(@Transactional),保证数据库操作和redis操作的原子性,先操作数据库再删除缓存。这种方式在即使没有锁的情况下也只有极低概率发送不一致的问题,因为发生这种情况要满足条件:(1)两个线程并行执行(2)线程1查询缓存刚好失效(3)就在线程1写入缓存时另一个线程先更新了数据库

        众所周知,redis的读写操作是非常快的,所以出现(3)的概率极低,且还需要满足3个条件,因此一般采用操作数据库再删除缓存模式。

三,缓存穿透

缓存穿透是指大量请求访问缓存中不存在的数据,导致这些请求直接访问数据库,从而给数据库带来巨大压力甚至崩溃。 

常见解决方案:

(1)缓存空值

        优点:简单便捷,维护成本低

        缺点:占用额外的内存空间,短暂不一致性(设置合适有效期可解决)

(2)布隆过滤器

        优点:占用内存少,无多余的key

        缺点:实现复杂,可能误判

这两种解决方案都是比较被动的解决方法,一般来说,对于恶意访问,我们更多通过增强id的复杂度,做好数据基础格式校验,加强用户权限校验等等。

实现非常简单,当缓存未命中且数据库中也没有查询到就将该id对应空值存入redis并设置有效期(比较短),然后在查询数据库之前添加判断非null的逻辑,阻止进入数据库

Shop shop = getById(id);
if (shop == null) {
   //5,数据库不存在,向redis中存入空值
   stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
   //返回错误
   return Result.fail("店铺不存在!");
}

五,缓存雪崩

定义:缓存层(如 Redis)突然大面积失效或不可用,导致大量请求直接穿透到数据库,引发数据库负载激增、响应变慢甚至宕机,最终造成系统整体不可用的连锁反应。

成因:

  1. 缓存集中过期
    大量缓存项在同一时间段内过期,导致同一时刻大量请求发现缓存失效,纷纷转向数据库查询。

  2. 流量峰值叠加缓存失效
    突发流量(如秒杀、大促)与缓存失效同时发生,数据库无法承受瞬时高并发请求。

常用解决方案:随机过期时间、集群高可用、流量控制等。

四,缓存击穿 

热点 key在过期瞬间被大量请求同时访问,导致请求直达数据库(如秒杀活动),引发数据库负载激增。

这里介绍两种解决方法:

1,互斥锁

当缓存失效时,仅允许一个线程执行数据库查询并重建缓存,其他线程等待缓存重建完成后再读取新缓存。通过 “互斥” 机制避免大量请求同时访问数据库。

(1)检查缓存是否存在,如果存在直接返回

(2)尝试获取锁,如果获取不成功说明有其他线程正在重建缓存,所以休眠一下,然后再尝试获取缓存。

(3)获取锁成功后还需要再次检测缓存,如果还是无缓存则执行缓存重建逻辑,查询数据库然后存入缓存。

    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            //存在直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的缓存是否是空值
        if (shopJson != null) {
            //如果是说明数据库中也没有这个值,返回空
            return null;
        }
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            //获取锁
            boolean isLock = tryLock(lockKey);
            //是否拿到锁
            if (!isLock) {
                //没有拿到说明有线程正在进行缓存重建
                Thread.sleep(50);
                //所以休眠一会然后重新查询缓存
                queryWithMutex(id);
            }
            //拿到锁检查一下缓存是否重建完毕
            String reShopJson = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(reShopJson)) {
                Shop reshop = JSONUtil.toBean(reShopJson, Shop.class);
                return reshop;
            }
            if (reShopJson != null) {
                return null;
            }
            //缓存还是不存在,查数据库
            shop = getById(id);
            //模拟延迟
            Thread.sleep(200);
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回错误
                return null;
            }
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            unlock(lockKey);
        }
        return shop;
    }

2,逻辑过期(实际不过期) 

不设置物理过期时间,而是在缓存中存储一个 “逻辑过期时间” 字段。当请求发现逻辑过期时,异步触发缓存更新,其他请求直接返回旧数据,避免阻塞。

(1)检查缓存是否存在,如果存在直接返回。

(2)解析逻辑过期时间,反序列化 JSON,获取expireTime,这里得到的对象是JSONObject需要手动转化为shop对象。

(3)判断是否过期,对比当前时间与expireTime,未过期,直接返回业务数据;已过期,触发异步更新缓存(这里采用开启新线程方式完成),同时返回旧数据(允许短暂不一致)。

    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shopJson)) {
            return null;
        }
        //判断是否过期 (逻辑过期)
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期,直接返回店铺信息
            return shop;
        }
        //已过期,需要缓存重建
        String lockKey = LOCK_SHOP_KEY + id;
        //获取锁
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            String reShop = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(reShop)) {
                return null;
            }
            //判断是否过期 (逻辑过期)
            RedisData redisDataCheck = JSONUtil.toBean(shopJson, RedisData.class);
            Shop shopCheck = JSONUtil.toBean((JSONObject) redisDataCheck.getData(), Shop.class);
            LocalDateTime expireTimeCheck = redisDataCheck.getExpireTime();
            if (expireTimeCheck.isAfter(LocalDateTime.now())) {
                //未过期,直接返回店铺信息
                return shopCheck;
            }
            //获取锁成功,开启独立线程,重建缓存
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //重建缓存
                try {
                    this.saveShopToRedis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }
            });
        }
        return shop;
    }

### 黑马程序员 Redis 实战教程中的商铺应用场景分析 Redis 是一种高性能的键值存储系统,在实际开发中广泛应用于缓存、会话管理以及实时数据处理等领域。对于商铺应用场景而言,Redis 的高效性和灵活性使其成为理想的选择。 #### 商铺应用中的短信登录功能 在商铺系统的用户登录模块中,短信验证码是一种常见的身份验证方式。通过集成 Redis 可以显著提升短信登录的功能效率和用户体验。具体来说,可以利用 Redis 存储用户的短信验证码及其过期时间[^1]。这种方式不仅减少了数据库的压力,还能够快速响应用户的请求并提供更安全的身份认证机制。 以下是基于 Session 和 Redis 实现短信登录的一个简单代码示例: ```python import redis # 初始化 Redis 客户端 r = redis.Redis(host='localhost', port=6379, db=0) def send_sms(phone_number, code): """发送短信验证码""" r.setex(f"sms:{phone_number}", 300, code) # 设置有效期为5分钟 def verify_code(phone_number, user_input_code): """验证短信验证码""" stored_code = r.get(f"sms:{phone_number}") if stored_code and stored_code.decode('utf-8') == user_input_code: return True return False ``` 此代码片段展示了如何使用 Redis 来保存短信验证码,并设置其有效期限以便后续验证操作。 #### 缓存击穿问题及解决方案 在商铺场景下,某些商品可能会因为促销活动而变成热点商品,从而引发大量的并发访问。如果这些热点商品的数据恰好从缓存中失效,则可能导致所谓的 **缓存击穿** 问题[^2]。这种情况下,大量请求直接打到后端数据库上,可能造成服务不可用或者性能下降。 针对这一现象,有几种常见解决方法: 1. 使用布隆过滤器提前判断是否存在该 key; 2. 对热点 key 进行加锁保护; 3. 配置永不过期或随机延长过期时间策略来平滑流量高峰。 下面是一个简单的 Python 脚本用于演示第二种方案——分布式互斥锁(Mutex Lock),防止多个线程同时尝试重新加载同一个缓存项: ```python import time from redis.lock import Lock lock_key = 'mutex_lock' with r.lock(lock_key, timeout=10): # 获取锁,超时时间为10秒 value = r.get('hot_item') if not value: # 如果未命中缓存则查询数据库并将结果写回缓存 data_from_db = fetch_data_from_database() r.setex('hot_item', 60 * 5, data_from_db) ``` 上述脚本确保只有第一个获取到锁的任务才会执行耗时较长的操作,其他等待中的任务可以直接返回最新更新后的缓存内容。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值