十年河东,十年河西,莫欺少年穷
学无止境,精益求精
缓存的出现解决了数据库压力的问题,但是当以下情况发生的时候,缓存就不在起到作用了,缓存穿透、缓存击穿、缓存雪崩这三种情况。
1. 缓存穿透:
我们的程序中用缓存的时候一般采取的是先去缓存中查询我们想要的缓存数据,如果缓存中不存在我们想要的数据的话,缓存就失去了作用(譬如缓存失效),这时我们就是需要伸手向DB库要数据,如果这种动作过多数据库就崩溃了。
这种情况需要我们去预防了,比如说:我们向缓存获取一个用户信息,但是故意去输入一个缓存中不存在的用户Key,这样就避过了缓存,把压力重新转移到数据上面了。
对于这种问题我们可以采取:
因为缓存查不到用户信息,数据库也查询不到用户信息,我们就把访问的数据进行缓存,这时候就可以避免重复访问,顺利把压力重新转向缓存中,有人会有疑问了,当访问的参数有上万个都是不重复的参数,并且都是可以躲避缓存的怎么办,我们同样把数据存起来设置一个较短过期时间清理缓存。
示例代码如下:
[HttpGet] [Route("RedisGet")] public IActionResult RedisGet(string key) { if (rd.KeyExists(key)) { /* * 如果缓存中存在,则直接返回结果 */ var result = rd.StringGet(key); return Ok(result); } else { /* * 如果缓存中不存在,则需要结合数据库进行查询,但必须采用相应的策略,防止恶意【缓存击穿】。 * 数据库查询部分, * 如果数据库查询到结果,则对结果进行缓存,并返回结果。 * 如果数据库查询不到结果,则对请求的数据进行缓存,防止缓存击穿。 * 除了上述比较被动的防御以外,我们还可以采取一段时间内限制请求次数来达到恶意攻击行为。 */ return Ok(); } }
2. 缓存击穿:
事情是这样的,对于一些设置了过期时间的缓存KEY,过期的时候,程序被高并发访问了(此时缓存已失效),这个时候由于缓存失效,访问压力也就转移到了数据库身上,高并发情况下,数据库往往扛不住那么多请求。
针对这种情况,我们可以使用互斥锁(Mutex)来解决问题,
互斥锁原理:通俗的描述就是,一万个用户访问了,但是只有一个用户可以拿到访问数据库的权限。
当这个用户拿到这个权限之后重新创建缓存,这个时候剩下的访问者因为没有拿到权限,就原地等待着去访问缓存。
逻辑上‘永不过期’:有人就会想了,我设置了过期时间,但我的系统中有一个定时的服务一直在跑,这个服务是用于判断缓存是否即将过期,如果发现即将过期的缓存,通过定时服务来更新缓存,这个时候缓存中的数据在逻辑上就会‘永不过期’了。
比如,定时服务每10分钟跑一次,但当我们发现缓存的过期时间小于10分钟了,我们通过服务来更新缓存,达到‘永不过期’的目的。
互斥锁解决方案:
using System; using System.Threading; namespace ConsoleApp1 { class shareRes { public static int count = 0; public static Mutex mutex = new Mutex(); } class IncThread { int number; public Thread thrd; public IncThread(string name, int n) { thrd = new Thread(this.run); number = n; thrd.Name = name; thrd.Start(); } void run() { Console.WriteLine(thrd.Name + "正在等待 the mutex"); //申请 shareRes.mutex.WaitOne(); Console.WriteLine(thrd.Name + "申请到 the mutex"); do { Thread.Sleep(1000); shareRes.count++; Console.WriteLine("In " + thrd.Name + "ShareRes.count is " + shareRes.count); number--; } while (number > 0); Console.WriteLine(thrd.Name + "释放 the nmutex"); // 释放 shareRes.mutex.ReleaseMutex(); } } class DecThread { int number; public Thread thrd; public DecThread(string name, int n) { thrd = new Thread(this.run); number = n; thrd.Name = name; thrd.Start(); } void run() { Console.WriteLine(thrd.Name + "正在等待 the mutex"); //申请 shareRes.mutex.WaitOne(); Console.WriteLine(thrd.Name + "申请到 the mutex"); do { Thread.Sleep(1000); shareRes.count--; Console.WriteLine("In " + thrd.Name + "ShareRes.count is " + shareRes.count); number--; } while (number > 0); Console.WriteLine(thrd.Name + "释放 the nmutex"); // 释放 shareRes.mutex.ReleaseMutex(); } } class Program { static void Main(string[] args) { IncThread mthrd1 = new IncThread("线程1 thread ", 5); DecThread mthrd2 = new DecThread("线程2 thread ", 5); mthrd1.thrd.Join(); mthrd2.thrd.Join(); Console.Read(); } } }
关于互斥锁解决方案,我们可以通过了解互斥锁(Mutex)来进行解决。
逻辑上‘永不过期’解决方案:
需要定义一个定时的服务,具体请参考 .netcore控制台->定时任务Quartz ,总之通过定时检测缓存过期时间来更新即将过期的缓存。
3. 缓存雪崩:
是指多种缓存设置了同一时间过期,这个时候大批量的数据访问来了,(缓存失效)数据库DB的压力又上来了。
解决方法在设置过期时间的时候,在过期时间的基础上增加一个随机数,尽可能的保证缓存不会大面积的同时失效,说白了,就是缓存的过期时间不能大批量相同。
以上便是使用Redis缓存应注意的三个方面及解决方案。
最后,
顺便贴出一个NetCore的缓存帮助类,封装的比较简单,但也是有用的,如下:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace FranchiseeCommon { /// <summary> /// Redis操作类 /// 老版用的是ServiceStack.Redis /// .Net Core使用StackExchange.Redis的nuget包 /// </summary> public class RedisHelper { //redis数据库连接字符串 private string _conn = "127.0.0.1:6379"; private int _db = 0; //静态变量 保证各模块使用的是不同实例的相同链接 private static ConnectionMultiplexer connection; /// <summary> /// 构造函数 /// </summary> public RedisHelper() { } /// <summary> /// 构造函数 /// </summary> /// <param name="db"></param> /// <param name="connectStr"></param> public RedisHelper(int db, string connectStr) { _db = db; _conn = connectStr; } /// <summary> /// 缓存数据库,数据库连接 /// </summary> public ConnectionMultiplexer CacheConnection { get { try { if (connection == null || !connection.IsConnected) { connection = new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(_conn)).Value; } } catch (Exception ex) { return null; } return connection; } } /// <summary> /// 缓存数据库 /// </summary> public IDatabase CacheRedis => CacheConnection.GetDatabase(_db); #region --KEY/VALUE存取-- /// <summary> /// 单条存值 /// </summary> /// <param name="key">key</param> /// <param name="value">The value.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> public bool StringSet(string key, string value) { return CacheRedis.StringSet(key, value); } /// <summary> /// 保存单个key value /// </summary> /// <param name="key">Redis Key</param> /// <param name="value">保存的值</param> /// <param name="expiry">过期时间</param> /// <returns></returns> public bool StringSet(string key, string value, TimeSpan? expiry = default(TimeSpan?)) { return CacheRedis.StringSet(key, value, expiry); } /// <summary> /// 保存多个key value /// </summary> /// <param name="arr">key</param> /// <returns></returns> public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] arr) { return CacheRedis.StringSet(arr); } /// <summary> /// 批量存值 /// </summary> /// <param name="keysStr">key</param> /// <param name="valuesStr">The value.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> public bool StringSetMany(string[] keysStr, string[] valuesStr) { var count = keysStr.Length; var keyValuePair = new KeyValuePair<RedisKey, RedisValue>[count]; for (int i = 0; i < count; i++) { keyValuePair[i] = new KeyValuePair<RedisKey, RedisValue>(keysStr[i], valuesStr[i]); } return CacheRedis.StringSet(keyValuePair); } /// <summary> /// 保存一个对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="obj"></param> /// <returns></returns> public bool SetStringKey<T>(string key, T obj, TimeSpan? expiry = default(TimeSpan?)) { string json = JsonConvert.SerializeObject(obj); return CacheRedis.StringSet(key, json, expiry); } /// <summary> /// 追加值 /// </summary> /// <param name="key"></param> /// <param name="value"></param> public void StringAppend(string key, string value) { ////追加值,返回追加后长度 long appendlong = CacheRedis.StringAppend(key, value); } /// <summary> /// 获取单个key的值 /// </summary> /// <param name="key">Redis Key</param> /// <returns></returns> public RedisValue GetStringKey(string key) { return CacheRedis.StringGet(key); } /// <summary> /// 根据Key获取值 /// </summary> /// <param name="key">键值</param> /// <returns>System.String.</returns> public string StringGet(string key) { try { return CacheRedis.StringGet(key); } catch (Exception ex) { return null; } } /// <summary> /// 获取多个Key /// </summary> /// <param name="listKey">Redis Key集合</param> /// <returns></returns> public RedisValue[] GetStringKey(List<RedisKey> listKey) { return CacheRedis.StringGet(listKey.ToArray()); } /// <summary> /// 批量获取值 /// </summary> public string[] StringGetMany(string[] keyStrs) { var count = keyStrs.Length; var keys = new RedisKey[count]; var addrs = new string[count]; for (var i = 0; i < count; i++) { keys[i] = keyStrs[i]; } try { var values = CacheRedis.StringGet(keys); for (var i = 0; i < values.Length; i++) { addrs[i] = values[i]; } return addrs; } catch (Exception ex) { return null; } } /// <summary> /// 获取一个key的对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public T GetStringKey<T>(string key) { return JsonConvert.DeserializeObject<T>(CacheRedis.StringGet(key)); } #endregion #region --删除设置过期-- /// <summary> /// 删除单个key /// </summary> /// <param name="key">redis key</param> /// <returns>是否删除成功</returns> public bool KeyDelete(string key) { return CacheRedis.KeyDelete(key); } /// <summary> /// 删除多个key /// </summary> /// <param name="keys">rediskey</param> /// <returns>成功删除的个数</returns> public long KeyDelete(RedisKey[] keys) { return CacheRedis.KeyDelete(keys); } /// <summary> /// 判断key是否存储 /// </summary> /// <param name="key">redis key</param> /// <returns></returns> public bool KeyExists(string key) { return CacheRedis.KeyExists(key); } /// <summary> /// 重新命名key /// </summary> /// <param name="key">就的redis key</param> /// <param name="newKey">新的redis key</param> /// <returns></returns> public bool KeyRename(string key, string newKey) { return CacheRedis.KeyRename(key, newKey); } /// <summary> /// 删除hasekey /// </summary> /// <param name="key"></param> /// <param name="hashField"></param> /// <returns></returns> public bool HaseDelete(RedisKey key, RedisValue hashField) { return CacheRedis.HashDelete(key, hashField); } /// <summary> /// 移除hash中的某值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public bool HashRemove(string key, string dataKey) { return CacheRedis.HashDelete(key, dataKey); } /// <summary> /// 设置缓存过期 /// </summary> /// <param name="key"></param> /// <param name="datetime"></param> public void SetExpire(string key, DateTime datetime) { CacheRedis.KeyExpire(key, datetime); } #endregion } }
NetCore配置文件为:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "tokenManagement": { "secret": "123456123456123456", "issuer": "webapi.cn", "audience": "WebApi", "accessExpiration": 120, "refreshExpiration": 60 }, "ConnectionStrings": { "aixueshi_temp1Context": "我的数据库连接;", "RedisConnectionStrings": "127.0.0.1:6379" } }
控制器端调用的代码如下:
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Threading.Tasks; using FranchiseeApi.Helper; using FranchiseeCommon; using FranchiseeDto; using FranchiseeDto.Franchisee; using FranchiseeInterface.Franchisee; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace FranchiseeApi { [Route("Api/V1/RedisApi")] [ApiExplorerSettings(GroupName = "V1")] public class RedisApiController : ControllerBase { private IConfigurationRoot ConfigRoot; private readonly RedisHelper rd; public RedisApiController(IConfiguration configRoot) { ConfigRoot = (IConfigurationRoot)configRoot; rd = new RedisHelper(0, ConfigRoot["ConnectionStrings:RedisConnectionStrings"]); } [HttpGet] [Route("RedisSet")] public IActionResult RedisSet() { rd.StringSet("sName", "陈卧龙"); return Ok(); } [HttpGet] [Route("RedisGet")] public IActionResult RedisGet(string key) { if (rd.KeyExists(key)) { /* * 如果缓存中存在,则直接返回结果 */ var result = rd.StringGet(key); return Ok(result); } else { /* * 如果缓存中不存在,则需要结合数据库进行查询,但必须采用相应的策略,防止恶意【缓存击穿】。 * 数据库查询部分, * 如果数据库查询到结果,则对结果进行缓存,并返回结果。 * 如果数据库查询不到结果,则对请求的数据进行缓存,防止缓存击穿。 * 除了上述比较被动的防御以外,我们还可以采取一段时间内限制请求次数来达到恶意攻击行为。 */ return Ok(); } } } }
如果您感觉还不错,就伸出贵手,点了赞吧。谢谢。
@天才卧龙的博客