1.缓存概念

  1.什么是缓存

    这里要讲到的缓存是服务端缓存,简单的说,缓存就是将一些实时性不高,但访问又十分频繁,或者说要很长时间才能取到的数据给存在内存当中,当有请求时直接返回,不用经过数据库或接口获取。这样就可以减轻数据库的负担。

  2.为什么要用缓存

    总的来说就是为了提高响应速度(用户体验度),减少数据库访问频率。

    在一个用户看来,软件使用的体验度才是关键,在对实时性要求不高的情况下,用户肯定会觉得打开界面的响应速度快,能保证平常工作的应用才是好的。因此为了满足这个需求,通过使用缓存,就可以保证满足在正常工作的前提下响应时间尽可能短。

    例如:当客户端向服务器请求某个数据时,服务器先在缓存中找,如果在缓存中,就直接返回,无需查询数据库;如果请求的数据不在缓存中,这时再去数据库中找,找到后返回给客户端,并将这个资源加入缓存中。这样下次请求相同资源时,就不需

      要连接数据库了。而且如果把缓存放在内存中,因为对内存的操作要比对数据库操作快得多,这样请求时间也会缩短。每当数据发生变化的时候(比如,数据有被修改,或被删除的情况下),要同步的更新缓存信息,确保用户不会在缓存取到旧的数据。

    如果没有使用缓存,用户去请求某个数据,当用户量和数据逐渐增加的时候,就会发现每次用户请求的时间越来越长,且数据库无时不刻都在工作。这样用户和数据库都很痛苦,时间一长,就有可能发生下以下事情:

      1.用户常抱怨应用打开速度太慢,页面经常无响应,偶尔还会出现崩溃的情况。

      2.数据库连接数满或者说数据库响应慢(处理不过来)。

      3.当并发量上来的时候,可能会导致数据库崩溃,使得应用无法正常使用。

 2.选用redis还是memcached

  简单说下这两者的区别,两者都是通过key-value的方式进行存储的,memcached只有简单的字符串格式,而redis还支持更多的格式(list、 set、sorted set、hash table ),缓存时使用到的数据都是在内存当中,

  不同的在于redis支持持久化,集群、简单事务、发布/订阅、主从同步等功能,当断电或软件重启时,memcached中的数据就已经不存在了,而redis可以通过读取磁盘中的数据再次使用。

  这里提高windows版的安装包:传送门          可视化工具:因文件太大无法上传到博客园。代码仓库中有,需要的私信哦~

 3.两者在netcore 中的使用

  memcached的使用还是相当简单的,首先在 startup 类中做以下更改,添加缓存参数  赋值给外部类来方便使用  

        public void configure(iapplicationbuilder app, ihostingenvironment env, imemorycache memorycache)
        {
            //....  省略部分代码
            demoweb.memorycache = memorycache;
            //....  省略部分代码
        }    

 

demoweb中的代码:
 public class demoweb
    {
      
        //....省略部分代码

        /// <summary>
        /// memorycache
        /// </summary>
        public static imemorycache memorycache { get; set; }

        /// <summary>
        /// 获取当前请求客户端ip
        /// </summary>
        /// <returns></returns>
        public static string getclientip()
        {
            var ip = httpcontext.request.headers["x-forwarded-for"].firstordefault()?.split(',')[0].trim();
            if (string.isnullorempty(ip))
            {
                ip = httpcontext.connection.remoteipaddress.tostring();
            }
            return ip;
        }
    }

  然后创建 memorycache 来封装些缓存的简单方法

    /// <summary>
    /// memorycache缓存
    /// </summary>
    public class memorycache
    {
        private static readonly hashset<string> keys = new hashset<string>();

        /// <summary>
        /// 缓存前缀
        /// </summary>
        public string prefix { get; }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="prefix"></param>
        public memorycache(string prefix)
        {
            prefix = prefix + "_";
        }

        /// <summary>
        /// 获取
        /// </summary>
        /// <typeparam name="t"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public t get<t>(string key)
        {
            return demoweb.memorycache.get<t>(prefix + key);
        }

        /// <summary>
        /// 设置 无过期时间
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        public void set(string key, object data)
        {
            key = prefix + key;
            demoweb.memorycache.set(key, data);
            if (!keys.contains(key))
            {
                keys.add(key);
            }
        }

        /// <summary>
        /// 设置
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        /// <param name="absoluteexpiration"></param>
        public void set(string key, object data, datetimeoffset absoluteexpiration)
        {
            key = prefix + key;
            demoweb.memorycache.set(key, data, absoluteexpiration);
            if (!keys.contains(key))
            {
                keys.add(key);
            }
        }

        /// <summary>
        /// 设置
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        /// <param name="absoluteexpirationrelativetonow"></param>
        public void set(string key, object data, timespan absoluteexpirationrelativetonow)
        {
            key = prefix + key;
            demoweb.memorycache.set(key, data, absoluteexpirationrelativetonow);
            if (!keys.contains(key))
            {
                keys.add(key);
            }
        }

        /// <summary>
        /// 设置
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        /// <param name="expirationtoken"></param>
        public void set(string key, object data, ichangetoken expirationtoken)
        {
            key = prefix + key;
            demoweb.memorycache.set(key, data, expirationtoken);
            if (!keys.contains(key))
            {
                keys.add(key);
            }
        }

        /// <summary>
        /// 设置
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        /// <param name="options"></param>
        public void set(string key, object data, memorycacheentryoptions options)
        {
            key = prefix + key;
            demoweb.memorycache.set(key, data, options);
            if (!keys.contains(key))
            {
                keys.add(key);
            }
        }

        /// <summary>
        /// 移除某个
        /// </summary>
        /// <param name="key"></param>
        public void remove(string key)
        {
            key = prefix + key;
            demoweb.memorycache.remove(key);
            if (keys.contains(key))
            {
                keys.remove(key);
            }
        }

        /// <summary>
        /// 清空所有
        /// </summary>
        public void clearall()
        {
            foreach (var key in keys)
            {
                demoweb.memorycache.remove(key);
            }
            keys.clear();
        }

    }

  其实接下来就可以直接使用缓存了,但为了方便使用,再建一个缓存类别的中间类来管理。

    public class usercache
    {
        private static readonly memorycache cache = new memorycache("user");

        private static timespan _timeout = timespan.zero;
        private static timespan timeout
        {
            get
            {
                if (_timeout != timespan.zero)
                    return _timeout;
                try
                {
                    _timeout = timespan.fromminutes(20);
                    return _timeout;
                }
                catch (exception)
                {
                    return timespan.fromminutes(10);
                }
            }
        }
        public static void set(string key,string cache)
        {
            if (string.isnullorempty(cache))
                return;

            cache.set(key, cache, timeout);
        }


        public static string get(string key)
        {
            if (string.isnullorempty(key))
                return default(string);

            return cache.get<string>(key);
        }
    }

  测试是否可以正常使用:代码与截图

        [httpget]
        [route("mecache")]
        public actionresult validtoken()
        {
            var key = "tkey";
            usercache.set(key, "测试数据");
            return succeed(usercache.get(key));
        }    

  可以清楚的看到 memorycache 可以正常使用。  

  那么接下来将讲到如何使用 redis 缓存。先在需要封装基础类的项目 nuget 包中添加  stackexchange.redis  依赖。然后添加redis 连接类

 internal class redisconnectionfactory
    {
        public string connectionstring { get; set; }
        public string password { get; set; }

        public connectionmultiplexer currentconnectionmultiplexer { get; set; }


        /// <summary>
        /// 设置连接字符串
        /// </summary>
        /// <returns></returns>
        public void setconnectionstring(string connectionstring)
        {
            connectionstring = connectionstring;
        }

        /// <summary>
        /// 设置连接字符串
        /// </summary>
        /// <returns></returns>
        public void setpassword(string password)
        {
            password = password;
        }

        public connectionmultiplexer getconnectionmultiplexer()
        {
            if (currentconnectionmultiplexer == null || !currentconnectionmultiplexer.isconnected)
            {
                if (currentconnectionmultiplexer != null)
                {
                    currentconnectionmultiplexer.dispose();
                }

                currentconnectionmultiplexer = getconnectionmultiplexer(connectionstring);
            }

            return currentconnectionmultiplexer;
        }


        private connectionmultiplexer getconnectionmultiplexer(string connectionstring)
        {
            connectionmultiplexer connectionmultiplexer;

            if (!string.isnullorwhitespace(password) && !connectionstring.tolower().contains("password"))
            {
                connectionstring += $",password={password}";
            }

            var redisconfiguration = configurationoptions.parse(connectionstring);
            redisconfiguration.abortonconnectfail = true;
            redisconfiguration.allowadmin = false;
            redisconfiguration.connectretry = 5;
            redisconfiguration.connecttimeout = 3000;
            redisconfiguration.defaultdatabase = 0;
            redisconfiguration.keepalive = 20;
            redisconfiguration.synctimeout = 30 * 1000;
            redisconfiguration.ssl = false;

            connectionmultiplexer = connectionmultiplexer.connect(redisconfiguration);

            return connectionmultiplexer;
        }
    }

  再添加redis客户端类

    /// <summary>
    /// redis client
    /// </summary>
    public class redisclient : idisposable
    {
        public int defaultdatabase { get; set; } = 0;

        private readonly connectionmultiplexer _client;
        private idatabase _db;

        public redisclient(connectionmultiplexer client)
        {
            _client = client;
            usedatabase();
        }

        public void usedatabase(int db = -1)
        {
            if (db == -1)
                db = defaultdatabase;
            _db = _client.getdatabase(db);
        }


        public string stringget(string key)
        {
            return _db.stringget(key).tostring();
        }


        public void stringset(string key, string data)
        {
            _db.stringset(key, data);
        }

        public void stringset(string key, string data, timespan timeout)
        {
            _db.stringset(key, data, timeout);
        }


        public t get<t>(string key)
        {
            var json = stringget(key);
            if (string.isnullorempty(json))
            {
                return default(t);
            }

            return json.tonettype<t>();
        }

        public void set(string key, object data)
        {
            var json = data.tojson();
            _db.stringset(key, json);
        }

        public void set(string key, object data, timespan timeout)
        {
            var json = data.tojson();
            _db.stringset(key, json, timeout);
        }

        /// <summary>
        /// exist
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool exist(string key)
        {
            return _db.keyexists(key);
        }

        /// <summary>
        /// delete
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool delete(string key)
        {
            return _db.keydelete(key);
        }

        /// <summary>
        /// set expire to key
        /// </summary>
        /// <param name="key"></param>
        /// <param name="expiry"></param>
        /// <returns></returns>
        public bool expire(string key, timespan? expiry)
        {
            return _db.keyexpire(key, expiry);
        }

        /// <summary>
        /// 计数器  如果不存在则设置值,如果存在则添加值  如果key存在且类型不为long  则会异常
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="expiry">只有第一次设置有效期生效</param>
        /// <returns></returns>
        public long setstringincr(string key, long value = 1, timespan? expiry = null)
        {
            var nubmer = _db.stringincrement(key, value);
            if (nubmer == 1 && expiry != null)//只有第一次设置有效期(防止覆盖)
                _db.keyexpireasync(key, expiry);//设置有效期
            return nubmer;
        }

        /// <summary>
        /// 读取计数器
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public long getstringincr(string key)
        {
            var value = stringget(key);
            return string.isnullorwhitespace(value) ? 0 : long.parse(value);
        }

        /// <summary>
        /// 计数器-减少 如果不存在则设置值,如果存在则减少值  如果key存在且类型不为long  则会异常
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public long stringdecrement(string key, long value = 1)
        {
            var nubmer = _db.stringdecrement(key, value);
            return nubmer;
        }



        public void dispose()
        {
            _client?.dispose();
        }
    }

  然后再添加redis连接生成工具类

    public static class redisfactory
    {
        private static readonly object locker = new object();

        private static redisconnectionfactory factory;

        private static void initredisconnection()
        {
            try
            {
                factory = new redisconnectionfactory();
                var connectionstring = demoweb.configuration["redis:connectionstring"];
#if debug
                connectionstring = "127.0.0.1:6379";
#endif
                factory.connectionstring = connectionstring;
                factory.password = demoweb.configuration["redis:pwd"];

            }
            catch (exception e)
            {
                loghelper.logger.fatal(e, "redis连接创建失败。");
            }
        }

        public static redisclient getclient()
        {
            //先判断一轮,减少锁,提高效率
            if (factory == null || string.isnullorempty(factory.connectionstring))
            {
                //防止并发创建
                lock (locker)
                {
                    initredisconnection();
                }
            }

            return new redisclient(factory.getconnectionmultiplexer())
            {
                defaultdatabase = demoweb.configuration["redis:defaultdatabase"].toint()
            };

        }
    }

  这里要使用到前面的静态扩展方法。请自行添加  传送门 ,还需要将 startup 类中的 configuration 给赋值到 demoweb中的 configuration 字段值来使用

  在配置文件 appsettings.json 中添加

  "redis": {
    "connectionstring": "127.0.0.1:6379",
    "pwd": "",
    "defaultdatabase": 0
  }

  再添加redis缓存使用类

    /// <summary>
/// redis缓存
/// </summary>
public class rediscache
{
private static redisclient _client;
private static redisclient client => _client ?? (_client = redisfactory.getclient());
private static string tokey(string key)
{
return $"cache_redis_{key}";
}
/// <summary>
/// 获取
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
public static t get<t>(string key)
{
try
{
var rediskey = tokey(key);
return client.get<t>(rediskey);
}
catch (exception e)
{
loghelper.logger.fatal(e, "rediscache.get \n key:{0}", key);
return default(t);
}
}
/// <summary>
/// 尝试获取
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="key"></param>
/// <param name="result"></param>
/// <returns></returns>
private static t tryget<t>(string key, out bool result)
{
result = true;
try
{
var rediskey = tokey(key);
return client.get<t>(rediskey);
}
catch (exception e)
{
loghelper.logger.fatal(e, "rediscache.tryget \n key:{0}", key);
result = false;
return default(t);
}
}
/// <summary>
/// 获取
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="key"></param>
/// <param name="setfunc"></param>
/// <param name="expiry"></param>
/// <param name="resolver"></param>
/// <returns></returns>
public static t get<t>(string key, func<t> setfunc, timespan? expiry = null)
{
var rediskey = tokey(key);
var result = tryget<t>(rediskey, out var success);
if (success && result == null)
{
result = setfunc();
try
{
set(rediskey, result, expiry);
}
catch (exception e)
{
loghelper.logger.fatal(e, "rediscache.get<t> \n key:{0}", key);
}
}
return result;
}
/// <summary>
/// 设置
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expiry"></param>
/// <returns></returns>
public static bool set<t>(string key, t value, timespan? expiry = null)
{
var allrediskey = tokey("||keys||");
var rediskey = tokey(key);
var allkeyredisvalue = client.stringget(allrediskey);
var keys = allkeyredisvalue.tonettype<list<string>>() ?? new list<string>();
if (!keys.contains(rediskey))
{
keys.add(rediskey);
client.set(allrediskey, keys);
}
if (expiry.hasvalue)
{
client.stringset(rediskey, value.tojson(), expiry.value);
}
else
{
client.stringset(rediskey, value.tojson());
}
return true;
}
/// <summary>
/// 重新设置过期时间
/// </summary>
/// <param name="key"></param>
/// <param name="expiry"></param>
public static void resetitemtimeout(string key, timespan expiry)
{
var rediskey = tokey(key);
client.expire(rediskey, expiry);
}
/// <summary>
/// exist
/// </summary>
/// <param name="key">原始key</param>
/// <returns></returns>
public static bool exist(string key)
{
var rediskey = tokey(key);
return client.exist(rediskey);
}
/// <summary>
/// 计数器 增加  能设置过期时间的都设置过期时间
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expiry"></param>
/// <returns></returns>
public static bool setstringincr(string key, long value = 1, timespan? expiry = null, bool needrest0 = false)
{
var rediskey = tokey(key);
try
{
if (expiry.hasvalue)
{
if (exist(key) && needrest0)
{
var exitvalue = getstringincr(key);
client.setstringincr(rediskey, value - exitvalue, expiry.value);
}
else
{
client.setstringincr(rediskey, value, expiry.value);
}
}
else
{
if (exist(key) && needrest0)
{
var exitvalue = getstringincr(key);
client.setstringincr(rediskey, value - exitvalue);
}
else
{
client.setstringincr(rediskey, value);
}
}
}
catch (exception e)
{
loghelper.logger.fatal($"计数器-增加错误,原因:{e.message}");
return false;
}
return true;
}
/// <summary>
/// 读取计数器
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static long getstringincr(string key)
{
var rediskey = tokey(key);
return client.getstringincr(rediskey);
}
/// <summary>
/// 计数器 - 减少
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static bool stringdecrement(string key, long value = 1)
{
var rediskey = tokey(key);
try
{
client.stringdecrement(rediskey, value);
return true;
}
catch (exception e)
{
loghelper.logger.fatal($"计数器-减少错误,原因:{e.message}");
return false;
}
}
/// <summary>
/// 删除
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static bool delete(string key)
{
var rediskey = tokey(key);
return client.delete(rediskey);
}
/// <summary>
/// 清空
/// </summary>
public static void clear()
{
//因为codis不支持keys之类的命令,所以只能自己记录下来,然后通过这个来清理。
var rediskey = tokey("||keys||");
var keys = client.get<list<string>>(rediskey);
var notexists = new list<string>();
foreach (var key in keys)
{
if (client.exist(key))
client.delete(key);
else
notexists.add(key);
}
if (notexists.count > 0)
{
keys.removeall(s => notexists.contains(s));
client.set(rediskey, keys);
}
}
}

  到这来基本就快可以拿来测试是否可以用了。但是前提是得把 redis 给运行起来。

  将上面的 redis安装包安装,并启动所安装文件夹中的 redis-server.exe 程序,若出现闪退情况,就运行 redis-cli.exe 程序,然后输入 shutdown 按下回车,重新运行 redis-server.exe 程序,就会出现这个界面。

  到这来,添加一个测试方法来看看效果。借助redis可视化工具查看结果如下

  测试完美成功,由于时间问题,上面 rediscache只有字符串的方法,没有加其它类型的方法。有需要的自己加咯~

 

  在下一篇中将介绍如何在netcore中如何使用 过滤器来进行权限验证

 

  有需要源码的在下方评论或私信~给我的svn访客账户密码下载,代码未放在github上。svn中新加上了redis安装包及可视化工具。