.net core WebApi使用Redis教程(五)Redis缓存应用缓存击穿概念及解决办法
小白浏览:4722024-01-11 09:40:54本文累计收益:0我也要赚钱

本文讲解Redis在应用缓存领域的使用方法,实例代码基于 .net core WebApi进行实现。实例代码下载链接:http://www.80cxy.com/Blog/ResourceView?arId=202401110950583321kEqrCT 

一、缓存击穿定义

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的Key突然失效了,无数请求访问会在瞬间给数据库带来巨大的冲击。

常见的两种解决方案:

(一)互斥锁:

缓存构建由特定线程创建,构建时获取互斥锁,其他线程再获取资源重建缓存时无法获取到互斥锁,只能等待重试,直到缓存重建成功。

优点:

没有额外的内存消耗,保证一致性,实现简单。

缺点:线程需要等待,性能受影响,可能有死锁风险。

(二)逻辑过期:

不在设置缓存TTL过期时间,在缓存数据中添加过期时间。缓存构建时获取互斥锁,构建由特定线程开启一个新线程构建,并将过期的数据返回,其他线程再获取资源重建缓存时无法获取到互斥锁,直接将过期数据返回,直到缓存构建完才能获取到新数据。

优点:线程无需等待,性能较好。

缺点:不保证一致性,有额外的内容消耗,实现复杂。

二、互斥锁实现代码
(一)代码逻辑流程

修改根据Id获取商铺的业务,基于互斥锁方式来解决缓存击穿问题。程序流程逻辑流程如下:

(二)实现代码
using Common.CacheManager;
using Common.Commons;
using Common.Const;
using Common.Enum;
using Common.Extensions;
using Domain.Entities;
using Domain.Repository;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Domain.Service
{
    public class RedisDomainService : IRedisDomainService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IServiceProvider serviceProvider;
        private readonly IRedisDomainRepostory redisRepostory;
        private readonly ICacheService cacheService;
        private ISession _session => _httpContextAccessor.HttpContext.Session;
        public RedisDomainService(IHttpContextAccessor httpContextAccessor, IRedisDomainRepostory redisRepostory, ICacheService cacheService, IServiceProvider serviceProvider = null)
        {
            _httpContextAccessor = httpContextAccessor;
            this.redisRepostory = redisRepostory;
            this.cacheService = cacheService;
            this.serviceProvider = serviceProvider;
        }
       
        #region Redis实现缓存
        public async Task<ResponseContent> GetShopById(string id)
        {
            ResponseContent response = new ResponseContent();
            //用互斥锁解决缓存击穿
            //Shop shop = await QueryWithMutex(id);
            if (shop == null)
                return response.Error(ResponseType.OperError);
            else
                return response.Ok(shop);
        }
        
        #region 根据ID获取商铺信息(用互斥锁解决缓存击穿代码)
        /// <summary>
        /// 根据ID获取商铺信息(用互斥锁解决缓存击穿代码)
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<Shop> QueryWithMutex(string id)
        {
            Shop shop = null;
            //1.从Redis缓存查询商品
            string shopJson = cacheService.Get(RedisConstants.CACHE_SHOP_KEY + id);
            //2.判断缓存是否存在
            if (!string.IsNullOrEmpty(shopJson))
            {
                //3.存在且不为空值直接返回
                return JsonConvert.DeserializeObject<Shop>(shopJson);
            }
            //判断命中的是否是空值(缓存穿透存入的空值)
            if (shopJson != null)
            {
                //返回错误信息
                return null;
            }
            //开始实现缓存重建
            //4.1获取互斥锁
            bool isLock = TryLock(RedisConstants.LOCK_SHOP_KEY + id);
            //4.2判断是否获取成功
            if (!isLock) {
                //4.3失败,则休眠并重试
                Thread.Sleep(50);
                return await QueryWithMutex(id);
            }
            //4.不存在,根据Id查询数据库
            shop = await redisRepostory.GetShopById(id);
            Thread.Sleep(200);//默认重建缓存延迟
            //5.不存在,返回错误
            if (shop == null)
            {
                //将空值写入Redis(解决缓存穿透)
                cacheService.Add(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL);
                //返回错误信息
                return null;
            }
            //6.存在写入Redis,并设置缓存超时时间
            cacheService.Add(RedisConstants.CACHE_SHOP_KEY + id, JsonConvert.SerializeObject(shop), RedisConstants.CACHE_SHOP_TTL);
            //7.释放互斥锁
            UnLock(RedisConstants.LOCK_SHOP_KEY + id);
            //8.返回
            return shop;
        }
        #endregion

        /// <summary>
        /// 获取互斥锁
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool TryLock(string key)
        {
            bool flag = cacheService.AddNx(key, "1");
            cacheService.Expire(key, 10);
            return flag;
        }
        /// <summary>
        /// 释放互斥锁
        /// </summary>
        /// <param name="key"></param>
        private void UnLock(string key)
        {
            cacheService.Remove(key);
        }
    }
}
三、逻辑过期实现代码
(一)逻辑过期代码实现逻辑流程

修改根据Id获取商铺的业务,基于逻辑过期方式来解决缓存击穿问题。程序流程逻辑流程如下:

(二)实现代码
using Common.CacheManager;
using Common.Commons;
using Common.Const;
using Common.Enum;
using Common.Extensions;
using Domain.Entities;
using Domain.Repository;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Domain.Service
{
    public class RedisDomainService : IRedisDomainService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IServiceProvider serviceProvider;
        private readonly IRedisDomainRepostory redisRepostory;
        private readonly ICacheService cacheService;
        private ISession _session => _httpContextAccessor.HttpContext.Session;
        public RedisDomainService(IHttpContextAccessor httpContextAccessor, IRedisDomainRepostory redisRepostory, ICacheService cacheService, IServiceProvider serviceProvider = null)
        {
            _httpContextAccessor = httpContextAccessor;
            this.redisRepostory = redisRepostory;
            this.cacheService = cacheService;
            this.serviceProvider = serviceProvider;
        }
       
        #region Redis实现缓存
        public async Task<ResponseContent> GetShopById(string id)
        {
            ResponseContent response = new ResponseContent();
            //用逻辑过期解决缓存击穿
            //生成Redis缓存
            //await SaveShop2Redis(id, 60);
            Shop shop = await QueryWithLogicalExpire(id);

            if (shop == null)
                return response.Error(ResponseType.OperError);
            else
                return response.Ok(shop);

        }
        #region 根据ID获取商铺信息(用逻辑过期解决缓存击穿代码)
        /// <summary>
        /// 根据ID获取商铺信息(用逻辑过期解决缓存击穿代码)
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<Shop> QueryWithLogicalExpire(string id)
        {
            Shop shop = null;
            //1.从Redis缓存查询商品
            string shopJson = cacheService.Get(RedisConstants.CACHE_SHOP_KEY + id);
            //2.判断缓存是否存在
            if (string.IsNullOrEmpty(shopJson))
            {
                //3.不存在直接返回
                return null;
            }
            //4.命中,需要先把Json反序列化为对象
            RedisData redisData = JsonConvert.DeserializeObject<RedisData>(shopJson);
            JObject jsonObject = (JObject)redisData.Data;
            shop = jsonObject.ToObject<Shop>();
            DateTime expireTime = redisData.ExpireTime;
            //5.判断是否过期
            int result = DateTime.Compare(DateTime.Now, expireTime);
            if (result < 0) {
                //5.1未过期直接返回店铺信息
                return shop;
            }
            //5.2已过期,需要缓存重建
            //6.缓存重建
            //6.1获取互斥锁
            bool isLock = TryLock(RedisConstants.LOCK_SHOP_KEY + id);
            //6.2判断是否获取成功
            if (isLock)
            {
                //6.3成功,开启独立线程实现缓存重建
                Thread t = new Thread(new ParameterizedThreadStart(async param => {
                    //重建缓存
                    await SaveShop2Redis(id, 20);
                    //释放锁
                    UnLock(RedisConstants.LOCK_SHOP_KEY + id);
                }));
                t.Start("");
            }
            //6.3返回
            return shop;
        }
        #endregion

       
        /// <summary>
        /// 获取互斥锁
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool TryLock(string key)
        {
            bool flag = cacheService.AddNx(key, "1");
            cacheService.Expire(key, 10);
            return flag;
        }
        /// <summary>
        /// 释放互斥锁
        /// </summary>
        /// <param name="key"></param>
        private void UnLock(string key)
        {
            cacheService.Remove(key);
        }
        /// <summary>
        /// 获取商铺信息并添加逻辑过期时间,添加到Redis
        /// </summary>
        /// <param name="id"></param>
        /// <param name="expireSeconds"></param>
        /// <returns></returns>
        private async Task SaveShop2Redis(string id, int expireSeconds)
        {
            using (var scope = serviceProvider.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<IRedisDomainRepostory>();
                //1.查询店铺数据
                Shop shop = await context.GetShopById(id);
                Thread.Sleep(200);
                //2.封装逻辑过期时间
                RedisData redisData = new RedisData();
                redisData.Data = shop;
                redisData.ExpireTime = DateTime.Now.AddSeconds(expireSeconds);
                //3.写入Redis
                cacheService.Add(RedisConstants.CACHE_SHOP_KEY + id, JsonConvert.SerializeObject(redisData));
            }
        }
        #endregion
    }
}

 

评论列表
发表评论
+ 关注