缓存管理之MemoryCache与Redis的使用
一、.memorycache介绍
memorycache是.net framework 4.0开始提供的内存缓存类,使用该类型可以方便的在程序内部缓存数据并对于数据的有效性进行方便的管理, 它通过在内存中缓存数据和对象来减少读取数据库的次数,从而减轻数据库负载,加快数据读取速度,提升系统的性能。
二、redis介绍
redis是一个开源的key-value存储系统,它支持的数据类型包括string(字符串)、 list(链表)、set(集合)、zset(sorted set --有序集合)和hashs(哈希)数据类型的相关操作
三、memorycache与redis的区别
1、性能方面:redis 只能使用单核(如果确实需要充分使用多核cpu的能力,那么需要在单台服务器上运行多个redis实例(主从部署/集群化部署),并将每个redis实例和cpu内核进行绑定),而 memorycache可以使用多核,所以每一个核上redis在存储小数据时比memcached性能更高。而存储大数据时,memcached性能要高于redis。
2、内存管理方面: memorycache使用预分配的内存池的方式,使用slab和大小不同的chunk来管理内存,item根据大小选择合适的chunk存储,内存池的方式可以省去申请/释放内存的开销,并且能 减小内存碎片产生,但这种方式也会带来一定程度上的空间浪费,并且在内存仍然有很大空间时,新的数据也可能会被剔除; redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片,在redis中,并不是所有的数据都一直存储在内存中的,当物理内存用完时,redis可以将一些很久没用到的value交换到磁盘。
3、数据持久化支持:redis虽然是基于内存的存储系统,但是它本身是支持内存数据的持久化的,而且提供两种主要的持久化策略:rdb快照和aof日志。而memorycache是不支持数据持久化操作的。
四、本系统中使用memorycache和redis
目标: 1、memorycache和redis使用无缝切换,统一接口,通过配置选择使用memorycache还是redis
2、使用redis时,减少对redis的读取(httpcontextaccessor配合redis使用)
实现:
memorycache我们采用easycaching(可以从git上获取:https://github.com/dotnetcore/easycaching),由于本身提供的接口不满足需求,所以我们直接下载到本地,将easycaching.core和easycaching.inmemory添加到项目中,如图所示:
在ieasycachingprovider添加接口
/// <summary> /// removes cached item by cachekey's contain. /// </summary> /// <param name="contain"></param> void removebycontain(string contain);
在easycachingabstractprovider.cs添加代码
public abstract void baseremovebycontain(string contain);
public void removebycontain(string contain) { var operationid = s_diagnosticlistener.writeremovecachebefore(new beforeremoverequesteventdata(cachingprovidertype.tostring(), name, nameof(removebyprefix), new[] { contain })); exception e = null; try { baseremovebycontain(contain); } catch (exception ex) { e = ex; throw; } finally { if (e != null) { s_diagnosticlistener.writeremovecacheerror(operationid, e); } else { s_diagnosticlistener.writeremovecacheafter(operationid); } } }
在defaultinmemorycachingprovider.async.cs中添加代码
public override void baseremovebycontain(string contain) { argumentcheck.notnullorwhitespace(contain, nameof(contain)); var count = _cache.removebycontain(contain); if (_options.enablelogging) _logger?.loginformation($"removebycontain : contain = {contain} , count = {count}"); }
在iinmemorycaching.cs中添加接口
int removebycontain(string contain);
在inmemorycaching.cs中实现接口
public int removebycontain(string contain) { var keystoremove = _memory.keys.where(x => x.contains(contain)).tolist(); return removeall(keystoremove); }
memorycache接口实现:memorycachemanager
using easycaching.core; using system; using system.threading.tasks; namespace tools.cache { /// <summary> ///内存管理 /// </summary> public partial class memorycachemanager : ilocker, istaticcachemanager { #region fields private readonly ieasycachingprovider _provider; #endregion #region ctor public memorycachemanager(ieasycachingprovider provider) { _provider = provider; } #endregion #region methods /// <summary> ///通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">,如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> public t get<t>(string key, func<t> acquire, int? cachetime = null) { if (cachetime <= 0) return acquire(); return _provider.get(key, acquire, timespan.fromminutes(cachetime ?? cachingdefaults.cachetime)).value; } /// <summary> /// 通过指定key获取缓存数据,不存在则返回null /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <returns></returns> public t get<t>(string key) { return _provider.get<t>(key).value; } /// <summary> ///通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">,如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> public async task<t> getasync<t>(string key, func<task<t>> acquire, int? cachetime = null) { if (cachetime <= 0) return await acquire(); var t = await _provider.getasync(key, acquire, timespan.fromminutes(cachetime ?? cachingdefaults.cachetime)); return t.value; } /// <summary> /// 设置缓存 /// </summary> /// <param name="key">key</param> /// <param name="data">value</param> /// <param name="cachetime">缓存时间(分钟)</param> public void set(string key, object data, int cachetime) { if (cachetime <= 0) return; _provider.set(key, data, timespan.fromminutes(cachetime)); } /// <summary> /// 判断key是否设置缓存 /// </summary> /// <param name="key">key</param> /// <returns>true表示存在;false则不存在</returns> public bool isset(string key) { return _provider.exists(key); } /// <summary> /// 执行某些操作使用独占锁 /// </summary> /// <param name="resource">独占锁的key</param> /// <param name="expirationtime">锁自动过期的时间</param> /// <param name="action">执行的操作</param> /// <returns>如果获取了锁并执行了操作,则为true;否则为false</returns> public bool performactionwithlock(string key, timespan expirationtime, action action) { if (_provider.exists(key)) return false; try { _provider.set(key, key, expirationtime); action(); return true; } finally { remove(key); } } /// <summary> ///通过key删除缓存数据 /// </summary> /// <param name="key">key</param> public void remove(string key) { _provider.remove(key); } /// <summary> /// 删除以prefix开头的缓存数据 /// </summary> /// <param name="prefix">prefix开头</param> public void removebyprefix(string prefix) { _provider.removebyprefix(prefix); } /// <summary> /// 删除所有包含字符串的缓存 /// </summary> /// <param name="contain">包含的字符串</param> public void removebycontain(string contain) { _provider.removebycontain(contain); } /// <summary> /// 删除所有的缓存 /// </summary> public void clear() { _provider.flush(); } public virtual void dispose() { } #endregion } }
redis实现:
cachingdefaults
using system; using system.collections.generic; using system.text; namespace tools.cache { public static partial class cachingdefaults { /// <summary> /// 缓存默认过期时间 /// </summary> public static int cachetime => 60; /// <summary> /// 获取用于保护key列表存储到redis的key(与启用persistdataprotectionkeysredis选项一起使用) /// </summary> public static string redisdataprotectionkey => "api.dataprotectionkeys"; } }
ilocker
using system; using system.collections.generic; using system.text; namespace tools.cache { public interface ilocker { /// <summary> /// 执行某些操作使用独占锁 /// </summary> /// <param name="resource">独占锁的key</param> /// <param name="expirationtime">锁自动过期的时间</param> /// <param name="action">执行的操作</param> /// <returns>如果获取了锁并执行了操作,则为true;否则为false</returns> bool performactionwithlock(string resource, timespan expirationtime, action action); } }
icachemanager
using system; using system.collections.generic; using system.text; namespace tools.cache { /// <summary> ///缓存接口 /// </summary> public interface icachemanager : idisposable { /// <summary> ///通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">,如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> t get<t>(string key, func<t> acquire, int? cachetime = null); /// <summary> /// 通过key获取指定缓存,如果不存在则返回null /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <returns></returns> t get<t>(string key); /// <summary> /// 设置缓存 /// </summary> /// <param name="key">key</param> /// <param name="data">value</param> /// <param name="cachetime">缓存时间(分钟)</param> void set(string key, object data, int cachetime); /// <summary> /// 判断key是否设置缓存 /// </summary> /// <param name="key">keym</param> /// <returns>true表示存在;false则不存在</returns> bool isset(string key); /// <summary> ///通过key删除缓存数据 /// </summary> /// <param name="key">key</param> void remove(string key); /// <summary> /// 删除以prefix开头的缓存数据 /// </summary> /// <param name="prefix">prefix开头的字符串</param> void removebyprefix(string prefix); /// <summary> /// 删除包含字符串的缓存 /// </summary> /// <param name="contain">包含的字符串</param> void removebycontain(string contain); /// <summary> /// 删除所有的缓存 /// </summary> void clear(); } }
perrequestcachemanager
using microsoft.aspnetcore.http; using system; using system.collections.generic; using system.linq; using system.text; using system.text.regularexpressions; using system.threading; using tools.componentmodel; namespace tools.cache { /// <summary> /// http请求期间用于缓存的管理器(短期缓存) /// </summary> public partial class perrequestcachemanager : icachemanager { #region ctor public perrequestcachemanager(ihttpcontextaccessor httpcontextaccessor) { _httpcontextaccessor = httpcontextaccessor; _locker = new readerwriterlockslim(); } #endregion #region utilities /// <summary> ///获取请求范围内共享数据的key/value集合 /// </summary> protected virtual idictionary<object, object> getitems() { return _httpcontextaccessor.httpcontext?.items; } #endregion #region fields private readonly ihttpcontextaccessor _httpcontextaccessor; private readonly readerwriterlockslim _locker; #endregion #region methods /// <summary> /// 通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> public virtual t get<t>(string key, func<t> acquire, int? cachetime = null) { idictionary<object, object> items; using (new readerwritelockdisposable(_locker, readerwritelocktype.read)) { items = getitems(); if (items == null) return acquire(); //i如果缓存存在,返回缓存数据 if (items[key] != null) return (t)items[key]; } //或者通过方法创建 var result = acquire(); if (result == null || (cachetime ?? cachingdefaults.cachetime) <= 0) return result; //设置缓存(如果定义了缓存时间) using (new readerwritelockdisposable(_locker)) { items[key] = result; } return result; } public t get<t>(string key) { idictionary<object, object> items; using (new readerwritelockdisposable(_locker, readerwritelocktype.read)) { items = getitems(); //i如果缓存存在,返回缓存数据 if (items[key] != null) return (t)items[key]; } return default(t);//没有则返回默认值null } /// <summary> /// 设置缓存 /// </summary> /// <param name="key">key</param> /// <param name="data">value</param> /// <param name="cachetime">缓存时间(分钟)</param> public virtual void set(string key, object data, int cachetime) { if (data == null) return; using (new readerwritelockdisposable(_locker)) { var items = getitems(); if (items == null) return; items[key] = data; } } /// <summary> /// 判断key是否设置缓存 /// </summary> /// <param name="key">key</param> /// <returns>true表示存在;false则不存在</returns> public virtual bool isset(string key) { using (new readerwritelockdisposable(_locker, readerwritelocktype.read)) { var items = getitems(); return items?[key] != null; } } /// <summary> /// 通过key删除缓存数据 /// </summary> /// <param name="key">key</param> public virtual void remove(string key) { using (new readerwritelockdisposable(_locker)) { var items = getitems(); items?.remove(key); } } /// <summary> /// 删除以prefix开头的缓存数据 /// </summary> /// <param name="prefix">prefix开头</param> public virtual void removebyprefix(string prefix) { using (new readerwritelockdisposable(_locker, readerwritelocktype.upgradeableread)) { var items = getitems(); if (items == null) return; //匹配prefix var regex = new regex(prefix, regexoptions.singleline | regexoptions.compiled | regexoptions.ignorecase); var matcheskeys = items.keys.select(p => p.tostring()).where(key => regex.ismatch(key)).tolist(); if (!matcheskeys.any()) return; using (new readerwritelockdisposable(_locker)) { //删除缓存 foreach (var key in matcheskeys) { items.remove(key); } } } } /// <summary> /// 删除所有包含字符串的缓存 /// </summary> /// <param name="contain">包含的字符串</param> public void removebycontain(string contain) { using (new readerwritelockdisposable(_locker, readerwritelocktype.upgradeableread)) { var items = getitems(); if (items == null) return; list<string> matcheskeys = new list<string>(); var data = items.keys.select(p => p.tostring()).tolist(); foreach(var item in data) { if(item.contains(contain)) { matcheskeys.add(item); } } if (!matcheskeys.any()) return; using (new readerwritelockdisposable(_locker)) { //删除缓存 foreach (var key in matcheskeys) { items.remove(key); } } } } /// <summary> /// 清除所有缓存 /// </summary> public virtual void clear() { using (new readerwritelockdisposable(_locker)) { var items = getitems(); items?.clear(); } } public virtual void dispose() { } #endregion } }
redis接口实现:rediscachemanager
using easycaching.core.serialization; using newtonsoft.json; using stackexchange.redis; using system; using system.collections.generic; using system.io; using system.linq; using system.net; using system.runtime.serialization.formatters.binary; using system.text; using system.threading.tasks; using tools.configuration; using tools.redis; namespace tools.cache { /// <summary> /// redis缓存管理 /// </summary> public partial class rediscachemanager : istaticcachemanager { #region fields private readonly icachemanager _perrequestcachemanager; private readonly iredisconnectionwrapper _connectionwrapper; private readonly idatabase _db; #endregion #region ctor public rediscachemanager(icachemanager perrequestcachemanager, iredisconnectionwrapper connectionwrapper, startupconfig config) { if (string.isnullorempty(config.redisconnectionstring)) throw new exception("redis 连接字符串为空!"); _perrequestcachemanager = perrequestcachemanager; _connectionwrapper = connectionwrapper; _db = _connectionwrapper.getdatabase(config.redisdatabaseid ?? (int)redisdatabasenumber.cache); } #endregion #region utilities protected byte[] serialize<t>(t value) { using (var ms = new memorystream()) { new binaryformatter().serialize(ms, value); return ms.toarray(); } } protected virtual ienumerable<rediskey> getkeys(endpoint endpoint, string prefix = null) { var server = _connectionwrapper.getserver(endpoint); var keys = server.keys(_db.database, string.isnullorempty(prefix) ? null : $"{prefix}*"); keys = keys.where(key => !key.tostring().equals(cachingdefaults.redisdataprotectionkey, stringcomparison.ordinalignorecase)); return keys; } protected virtual ienumerable<rediskey> getcontainkeys(endpoint endpoint, string contain = null) { var server = _connectionwrapper.getserver(endpoint); var keys = server.keys(_db.database, string.isnullorempty(contain) ? null : $"*{contain}*"); keys = keys.where(key => !key.tostring().equals(cachingdefaults.redisdataprotectionkey, stringcomparison.ordinalignorecase)); return keys; } protected virtual async task<t> getasync<t>(string key) { if (_perrequestcachemanager.isset(key)) return _perrequestcachemanager.get(key, () => default(t), 0); var serializeditem = await _db.stringgetasync(key); if (!serializeditem.hasvalue) return default(t); var item = jsonconvert.deserializeobject<t>(serializeditem); if (item == null) return default(t); _perrequestcachemanager.set(key, item, 0); return item; } protected virtual async task setasync(string key, object data, int cachetime) { if (data == null) return; var expiresin = timespan.fromminutes(cachetime); var serializeditem = jsonconvert.serializeobject(data); await _db.stringsetasync(key, serializeditem, expiresin); } protected virtual async task<bool> issetasync(string key) { if (_perrequestcachemanager.isset(key)) return true; return await _db.keyexistsasync(key); } #endregion #region methods /// <summary> ///通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">,如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> public async task<t> getasync<t>(string key, func<task<t>> acquire, int? cachetime = null) { if (await issetasync(key)) return await getasync<t>(key); var result = await acquire(); if ((cachetime ?? cachingdefaults.cachetime) > 0) await setasync(key, result, cachetime ?? cachingdefaults.cachetime); return result; } /// <summary> /// 通过key获取缓存 /// </summary> /// <typeparam name="t">>缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <returns>通过key获取到的特定的数据</returns> public virtual t get<t>(string key) { if (_perrequestcachemanager.isset(key)) return _perrequestcachemanager.get(key, () => default(t), 0); var serializeditem = _db.stringget(key); if (!serializeditem.hasvalue) return default(t); var item = jsonconvert.deserializeobject<t>(serializeditem); if (item == null) return default(t); _perrequestcachemanager.set(key, item, 0); return item; } /// <summary> ///通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">,如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> public virtual t get<t>(string key, func<t> acquire, int? cachetime = null) { if (isset(key)) return get<t>(key); var result = acquire(); if ((cachetime ?? cachingdefaults.cachetime) > 0) set(key, result, cachetime ?? cachingdefaults.cachetime); return result; } /// <summary> /// 设置缓存 /// </summary> /// <param name="key">key</param> /// <param name="data">value</param> /// <param name="cachetime">缓存时间(分钟)</param> public virtual void set(string key, object data, int cachetime) { if (data == null) return; var expiresin = timespan.fromminutes(cachetime); var serializeditem = jsonconvert.serializeobject(data); _db.stringset(key, serializeditem, expiresin); } /// <summary> /// 判断key是否设置缓存 /// </summary> /// <param name="key">keym</param> /// <returns>true表示存在;false则不存在</returns>s> public virtual bool isset(string key) { if (_perrequestcachemanager.isset(key)) return true; return _db.keyexists(key); } /// <summary> ///通过key删除缓存数据 /// </summary> /// <param name="key">key</param> public virtual void remove(string key) { if (key.equals(cachingdefaults.redisdataprotectionkey, stringcomparison.ordinalignorecase)) return; _db.keydelete(key); _perrequestcachemanager.remove(key); } /// <summary> /// 删除所有包含字符串的缓存 /// </summary> /// <param name="contain">包含的字符串</param> public void removebycontain(string contain) { _perrequestcachemanager.removebycontain(contain); foreach (var endpoint in _connectionwrapper.getendpoints()) { var keys = getcontainkeys(endpoint, contain); _db.keydelete(keys.toarray()); } } /// <summary> /// 删除以prefix开头的缓存数据 /// </summary> /// <param name="prefix">prefix开头</param> public virtual void removebyprefix(string prefix) { _perrequestcachemanager.removebyprefix(prefix); foreach (var endpoint in _connectionwrapper.getendpoints()) { var keys = getkeys(endpoint, prefix); _db.keydelete(keys.toarray()); } } /// <summary> /// 删除所有的缓存 /// </summary> public virtual void clear() { foreach (var endpoint in _connectionwrapper.getendpoints()) { var keys = getkeys(endpoint).toarray(); foreach (var rediskey in keys) { _perrequestcachemanager.remove(rediskey.tostring()); } _db.keydelete(keys); } } public virtual void dispose() { } #endregion } }
istaticcachemanager redis和memorycache统一暴露的接口
using system; using system.collections.generic; using system.text; using system.threading.tasks; namespace tools.cache { /// <summary> ///用于在http请求之间进行缓存的管理器(长期缓存) /// </summary> public interface istaticcachemanager : icachemanager { /// <summary> ///通过key获取缓存,如果没有该缓存,则创建该缓存,并返回数据 /// </summary> /// <typeparam name="t">缓存项type</typeparam> /// <param name="key">缓存 key</param> /// <param name="acquire">,如果该key没有缓存则通过方法加载数据</param> /// <param name="cachetime">缓存分钟数; 0表示不缓存; null则使用默认缓存时间</param> /// <returns>通过key获取到的特定的数据</returns> task<t> getasync<t>(string key, func<task<t>> acquire, int? cachetime = null); } }
配置是否使用redis作为缓存,默认使用memorycache
"cache": { "redisenabled": true, "redisdatabaseid": "", "redisconnectionstring": "127.0.0.1:6379,ssl=false", "useredistostoredataprotectionkeys": true, "useredisforcaching": true, "useredistostorepluginsinfo": true }
读取配置扩展servicecollectionextensions
using microsoft.extensions.configuration; using microsoft.extensions.dependencyinjection; using system; using system.collections.generic; using system.text; namespace infrastructure.common.extensions { public static class servicecollectionextensions { public static tconfig configurestartupconfig<tconfig>(this iservicecollection services, iconfiguration configuration) where tconfig : class, new() { if (services == null) throw new argumentnullexception(nameof(services)); if (configuration == null) throw new argumentnullexception(nameof(configuration)); var config = new tconfig(); configuration.bind(config); services.addsingleton(config); return config; } } }
依赖注入:我们这里采用autofac来实现依赖注入,在startup.cs中的configureservices方法中添加:
services.addsingleton<ihttpcontextaccessor, httpcontextaccessor>(); var build = new configurationbuilder().setbasepath(directory.getcurrentdirectory())//setbasepath设置配置文件所在路径 .addjsonfile("appsettings.json"); var configroot = build.build(); var config = services.configurestartupconfig<startupconfig>(configroot.getsection("cache")); var builder = new containerbuilder(); #region 自动判断是否使用redis,ture则使用redis,否则使用本机内存缓存 if (config.redisenabled) { //services. builder.registertype<redisconnectionwrapper>() .as<ilocker>() .as<iredisconnectionwrapper>() .singleinstance(); } //static cache manager if (config.redisenabled && config.useredisforcaching) { builder.registertype<rediscachemanager>().as<istaticcachemanager>() .instanceperlifetimescope(); } else { builder.registertype<memorycachemanager>() .as<ilocker>() .as<istaticcachemanager>() .singleinstance(); } #endregion services.addeasycaching(option => { //use memory cache option.useinmemory("default"); }); var container = builder.build(); return new autofacserviceprovider(container);//autofac 接管.netcore默认di
在configure方法中添加:
app.useeasycaching();
下一篇: GO基础之结构体
推荐阅读
-
缓存管理之MemoryCache与Redis的使用
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之八MemoryCache与redis缓存的使用
-
十:redis之HyperLogLog的使用与应用场景
-
缓存管理之MemoryCache与Redis的使用
-
redis之django-redis的简单缓存使用
-
Redis数据结构之链表与字典的使用
-
Redis之sql缓存的具体使用
-
Node.js学习之NVM版本管理器的安装与简单使用(NVM)
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之八MemoryCache与redis缓存的使用
-
使用Redis作为Spring Boot的缓存管理器步骤