• NET 企业缓存应用总结


    缓存是个宽泛且永不过时的话题,如何使用好缓存还是值得细细琢磨,(这部分内容也是我在招聘的时候对应聘者的提问,能回答出来的有,但让人满意的很少),现在就我在工作中实际用的缓存经验及与缓存相关的经验总结为3个部分,缓存设计思路NET平台下的缓存分布式缓存,一来自己备用,二来和园子的朋友交流,以及如果有需要的朋友可以作为参考。

    第一部分 缓存设计思路

    1.   缓存技术选择

    今天缓存技术可以说是多样化了,给使用者提供了很好的选择余地,正是如此对于缓存技术的选择也尤为重要,对于选择缓存技术,这几点是通用的:

    1. 是什么
    2. 主要能解决什么问题
    3. 它的局限性在哪里

    选用什么样的缓存技术取决你的业务需要,NET自带的缓存,以及现在成熟的分布式缓存都有自身的优缺点,一定要弄清楚他们解决问题的对象以及你能承受意外情况(数据丢失,网络阻塞等情况)。

    2.   使用规划

    2.1 对存储数据大小,存储时间X 2估算出硬件配置(缓存对内存,CPU可以配置高点)。

    2.2  监控缓存服务器的运行状况以及缓存系统自身运行情况(重要),zabbix系统可以监控linux,windows机器运行情况,但缓存系统自身运行状况需要自己实现,现在很多NoSql服务都提供了Stats,Info,serverStatus诸如此类的命令,只需后台线程定时读出对应的值和你配置的阀值对比即可,如果超过阀值则预警,这样你就可以时刻知道你缓存系统运行情况了。(开发套完善的监控系统也是很多公司的基础服务,淘宝,大众都有自己监控系统)

    2.3  任何操作涉及到磁盘,磁带IO都会出现性能瓶颈,不同是这些产品解决这种性能瓶颈能力的程度不同,但不可能做到从磁盘IO操作还是保持和内存操作一样的性能,所以一定要规划好数据量,在内存中存活时间(如果你的数据一直能装在内存里那用不用缓存都无所谓了),所有noSQL产品都有配置文件这里有些参数(比如,内存大小分配,冷热数据策略)至关重要。

    2.4   如果缓存服务不可用,你的系统该如何切换业务流程很重要。(我们公司项目在资源合并中使用memcached,有次memcached出现了问题由于资源合并导致了前端页面样式乱掉)

    2.5 对应业务系统比较复杂的缓存应用我建议最好把缓存业务参数独立出来做出配置,这样无论开关缓存,过期时间调整,key运行参数等值如需变动我们修改了配置就Ok了。

    2.6  对于分布式缓存都有自己的客户端,我建议不要直接引用你的系统调用这些API,做个二次适配或简化下外观,统一规划下调用接口,这样业务调用也方便,即使有变动那么只需修改了这个适配器就Ok了

    2.7  注意细节:在这些缓存客户端启动时大多有个启动代码,你如果声明了全局(private static xx=XX.Init此类的代码)最后检查配置文件是否正确,如果错误你的程序将会时不时异常严重点将导致站点挂掉。

    2.8 对与客户端API你有时间应该全部看下客户端实现代码,从这里你能学到很多东西,但无论如何你都应该看看test,doc,changlog下面的文档,对你在使用这个客户端有很大的帮助。

    2.9  对于服务端实现代码,现在大多项目现在都开源你可以打开看看里面的源代码。服务端,客户端有些算法你稍微修改了就可以独立使用了,是个很好的捷径。

    2.1.1 对于分布式Nosql存储服务端架构在部署前一定要关注下官网的wiki,有时间最好做个压力测试看看是否满足你的性能指标,也可以参考下网上对你选用的NoSql技术评价及使用。

    2.1.2 如果使用Nosql存储必然面临升级问题,现在Nosql更新都比较快,我建议你最好不要跟随官方第一天发布新版本你第二天就升级,多看看官方升级后changlog文件,及升级过程中注意的问题,一般当新版发布后都有过渡期这期间如果出现问题又没用使用经验的话那就麻烦了(如果你有专业维护团队那另当别论),当升级过些时间(几个月)这样google下一些使用经验就有放出来了,包括官方的更新文档。

    3.  数据缓存的位置

    这个位置是指物理层的位置,即:远程,本地,客户端。看似很简单的两个位置,其实是有权重的。服务端——远程(分布式)缓存:

    1. 最大好处就是可以实现缓存数据共享,
    2. 便于数据管理,由专门的机器提供数据存储
    3. 增加网络带宽开销
    4. 增加数据序列化成本
    5. 增加服务器成本
    6. 如果网络堵塞那么可能出现获取缓存数据超时
    7. 可以多技术平台共享(服务端什么语言开发的,客户端无需关注,只需个客户端即可数据存储获取)

    服务端——本地缓存:

    1. 无网络开销
    2. 可以本进程内,也可以跨进程缓存数据
    3. 无序列化开销
    4. 增加数据管理难度,一旦数据不一致增加排错难度
    5. 无法实现数据共享
    6. 一个业务系统机器其配置一般情况是比不过专门存储的服务器,本地缓存增加系统的内存。

    客户端——浏览器缓存

    1. 内容缓存到客户端浏览器直接获取数据,这是最快的方式
    2. 由于数据缓存到客户端,如果用户投诉说“XXX问题”这也增加了排查问题的难度

    4.   分析在哪些场景中需要使用缓存

    任何一个技术案例使用的前提都有“场景”这个约束条件,在适当的“场景”下这个技术使用结果是积极的,反之则是消极的。那么如何确定使用缓存的场景,场合呢。我在实际工作中这样分析的。

    1. 相对静态的对象,如:页面,比如很多网页上有公司介绍内容;如:业务配置对象从磁盘加载完后应该放入缓存。(这里涉及到缓存修改通知见后面“缓存依赖”分析);如:网站图片

         2. 业务计算前,后的的对象——前是指业务计算前的参数类型,后是指业务完成后产生的数据对象,如:用户访问过的对象或相关对    象,现在很多网站都有“你浏览过或浏览此商品的人还浏览过”,当然这个过程实现稍微复杂点。;如:构建列表对象,且构建一次相当耗时(这个标准视业务而定)的场合;如:数据访问层ADO.NET 访问数据库参数类型列表(不是参数变量),应该缓存(如果还在用户ADO.NET的话)。

    5.   确定要缓存的数据格式

          1.  在存储数据格式式尽量要粗粒度,尽量是扁平化的数据对象,如果你的缓存对象嵌套了太深的层次这样在序列化是很耗性能的。

           2.  保存value的数据结构形式主要是二进制,或Json其宗旨是减少网络传输,提高传输效率。二进制是较多分布式存储的主要格式,更多序列化工具请参看在序列化对象的有很多序列化工具,比如Redis客户端StackService,使用的StackService.Text,google的protocolBuffer这个API很变态的,在官方序列化对比结果中protocolBuffer(注意:pb NET有两个版本ProtoBuf.NET,protobuf-csharp-port)序列化后的体积遥遥领先其他API序列化后的体积,Newtonsoft.Json.Net35 Json格式序列化,NET自带的javascript Serialization,看看以下对比

    6.   缓存KEY设计

    其实缓存Key是个再简单不过的东西了,但是要做到规范化还是有些细节要注意。设计这个Key大致分以下几种情况。

    1. 长短不一的,没有明显意义的key,这种命名大多是很随意的。
    2. 长度统一的key,比如hash(key)出来,这种key 很好的统一了key的长度。
    3. 具有标识性的key,比如userId_DateTime,很容易辨认出用户某个时段的数据

    以上3中key设计,第一种很不规范不值得取,如果你是这样做的那应该尽快改正这个做法,2,3中视情况而定。不管哪种key,其长度如果太长(大于32位)那么这个肯定是不可取的。对于缓存key 我们可以指定一个通用的方法,如:static string GetHahsKey(this object[] val){…….}返回md5后的key值即可。

    7.   确定数据过期策略

    数据过期是缓存的一个基本功能,如何确定过期数据却是跟使用缓存的对象相关的,基于时间过期,这个测试过期的时间最好使用TimeSpan相对时间来设置,不要使用DateTime绝对时间设置

    8.   如何加载缓存数据

    1. 预加载,即应用程序启动就加载缓存数据,我认为除必须先加载(如,业务基础配置,应用程序配置)外,最好都采用延迟加载,因为预先加载可以导致你程序很长时间才会运行再严重点可能导致服务器岩机。

    2.延迟加载,即什么用什么时间加载,我的项目里绝大部分是这样加载数据的。

    9.  如何存储缓存数据

    1.  顺序存储,即一个业务操作完成即把数据写入缓存媒介,这样做有个小的瑕疵,在返回业务处理结果前是需要等待缓存写入完毕的,如果因为写入缓存延时那么可能造成用户体验不好。

    2.  异步存储,这也是我在工作中用到最多缓存写入方式,这样做就是可以及时做

    10. 自定义缓存过期策略

    1. 基于时间过期
    2. 基于通知的过期策略

    第二部分 NET平台缓存

    1.页面级缓存:

          1.   声明式设置OutputCache指令,其中Location参数可以申明缓存的位置如果设置为服务端,所以你要确定你缓存的数据大小,缓存的时间,即web服务    器内存大小。在使用页面级的缓存,VaryByParam是使缓存变化的参数,Duration是缓存的时间(秒),VaryByControl是缓存页面的某些部分;

    更多OutputCache使用请参阅,http://msdn.microsoft.com/zh-cn/library/hdxfb6cy(v=vs.80).aspx

         2.   编程式使用HttpCachePolicy实现缓存,(MSDN: 包含用于设置缓存特定的 HTTP 标头的方法和用于控制 ASP.NET 页输出缓存的方法),通  过Response.Cache 可以获取HttpCachePolicy实例。

    注:如果需要关闭页面缓存 Response.Cache.SetCacheability(HttpCacheability.NoCache);这种方式比较简单

    更多HttpCachePolicy使用请参阅,http://msdn.microsoft.com/en-us/library/system.web.httpcachepolicy.aspx

    个人观点:大部分页面输出缓存直接以声明式设置缓存即可,一个页面(列表页)最好做到功能单一,这样设置缓存也会很方便,如果有多个功能(比如头部是用户信息,广告等)需要组合后再并到一个页面,那么选用用户控件比较合适,万不可直接将所有功能揉杂在一页面上,这样无论开发亦或是维护都是很麻烦的   

     

    3. ViewState

            1.  对于ViewState第一映像是页面很大一串base64编码的字符,这与使用服务器控件的数量是成正比的。viewState作用是保存在会发postback期间恢复控件状态的信息,比如点击了submit按钮后之前文本框还会存在点击这个按钮之前的信息,除了保存状态外还有个不好的名声就是增加了页面体积服务器要发送和接收viewState并验证这样会影响网站性能。

            2.  override void SavePageStateToPersistenceMedium(object state),override object LoadPageStateFromPersistenceMedium()重写这两个方法是可以把viewState保存到服务器端的,我早些时候做网站是这样的,后来还是放弃了这种方式,因为不使用服务器控件,一样可以把网站做出来,不是吗?

            3.  ViewStateException,这个异常“表示当无法加载或验证视图状态时引发的异常。无法继承此类, 视图状态异常可能是消息身份验证代码 (MAC) 验证错误的结果。如果页属性 enableViewStateMac 设置为 true,则视图状态信息用 MAC 标识符进行编码。当视图状态信息回发到服务器时,该页会验证编码以确保它未被用户更改。如果该页无法验证视图状态信息的 MAC 编码,则会引发 HttpException 异常,并将创建 ViewStateException 对象作为内部异常。

    若要 MAC 验证成功,视图状态信息在发送和接收时必须由相同的密钥进行散列。在网络场中,每一个服务器上的计算机密钥都应该设置为公共密钥。

    ”——MSDN,这里之所以提到这个问题,是因为在我公司的监控系统中经常会报ViewStateException异常,后来禁止了viewState,把不必要的服务器控件替换为html控件,现在我们网站前端都很少甚至不使用服务器控件了,也就无viewStateException了,而且页面体积较使用服务器控件之前大大缩减。所以是不是用服务器控件最好做个权衡,分析下利弊再使用。

    4. Cookie

         1.   说真的Cookie我在业务中使用cookie的时候都是保存很少的信息比如一个Guid标志,而且很少持久化,大多是保存在客户端浏览进程中,这样浏览器关闭cookie自动清除掉。(插个话题,在公司我之前维护discuze!net社区的时候用户登录信息保存cookie中设置了过期时间但出现了过期了登录信息还在,(内部)用户想切换用户登录只有把cookie文件删了才可以登录)

         2.  业务上使用Cookie时最好给信息加密,直接的做法在web.config中配置加密算法,另外是把加密单独提炼出来读写cookie先调用这个加密解密方法,不要加入太多的信息,另外如果能不持久化那就直接保存客户端浏览器进程中,这样浏览器关闭cookie也就没了,如果浏览器每次太大的cookie不但影响网站响应速度,而且也增加了安全风险这是不争的事实。

        3.   如果多个站点需要Cookie共享,我这项目中是这样做的,在不同站点下保存客户端标识,然后把这个标志待定服务器验证,数据完全存储在服务端缓存或持久化。

     

    5.  HttpSessionState

         1.  “提供对会话状态值以及会话级别设置和生存期管理方法的访问。”——MSDN;“生命周期当用户发出第一个请求时创建,并一直延续到用户关闭会话“——APS.NET2.0技术内幕,通过以上对HttpSessionState的定义我们应该知道这个对保存的东西是随请求生命期而存在的。使用Session有[Off|InProc|StateServer|SQLServer|Custom]5种模式,一种是默认的就是“InProc模式 ”数据存储在w3wp.exe进程中。其值默认20分钟超时,个人任务这个超时时间不是那么合理,并不是每个值需要保存这么长时间的,这会照成内存浪费。

         2. 重写SessionStateStoreProviderBase类即可实现Custom模式,自定义Session数据存储位置(数据库、分布式),这也是我现在项目里用的这个模式,这样做有个好处我们可以统一管理Session并且跟踪网站使用Session的情况,分析出Session保存的哪些数据是不是必要的。

         3. 更多Session的使用请参阅,ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/dv_ASPNETgenref/html/bda6fb8c-0076-43e3-9ce2-8cf1f8bdaa7d.htm。http://msdn.microsoft.com/en-us/library/ms178581(v=vs.100).aspx

     

    6.  HttpRuntime.Cache,System.Web.Caching.Cache

         1. HttpRuntime.Cache,System.Web.Caching.Cache这两个类开发NET的童鞋应该都熟悉的,通过反编译可以看到System.Web.Caching.Cache Cache对象是从HttpRuntime.Cache 获取的,这是他们的相同点;不同点是HttpRuntime和HttpContext两个对象,HttpRunTime对象MSDN定义“为当前应用程序提供一组 ASP.NET 运行时服务。“,httpContext 对象MSDN定义” 封装有关个别 HTTP 请求的所有 HTTP 特定的信息。“,由此很明显了HttpRunTime.Cache在web及非web环境中都可以使用,而HttpContext.Current.Cache局限于web环境中使用,所以如果是非web程序中应该是使用HttpRunTime.Cache,在web应用程序中二者都可以使用

         2.  Insert ,Add方法,有个细节要注意“Insert 方法,并向缓存中添加与现有项同名的项,则将从缓存中删除该旧项”——MSDN,“Add方法,如果 Cache 中已保存了具有相同 key 参数的项,则对此方法的调用将失败,不会替换该项,并且不会引发异常”——MSDN。简而言之,Insert 增加或者替换,Add 只增加不替换。

         3.   缓存依赖。NET提供了基于文件,对象过期,数据库数据表动等依赖功能很强大,但这个功能需要区别对待,如果你缓存了数据库里的一张表,我个人认为不应该添加缓存依赖这个功能,看过NetPetShop项目源码的童鞋知道这个项目里缓存了数据库的一张表(具体什么表,我现在也记不清了)并且添加了缓存依赖,期结果在数据库自动构建了4张(映像中)表,几个存储过程,在这个表上创建了个触发器,当你修改这个表数据时触发器自动记录更新,应用程序根据这个记录就能确定数据是否修改,并调用过期回调函数,这种做法,如果再一个表数据量大的时候是有问题的,我记得当时我向公司DBA询问时,当时他毫不犹豫的否决了这个做法,理由很简单“影响性能”后来我想想这样做确实是有隐藏的问题的,当数据增长到一个度的时候在表设置个触发器确实值得权衡(比如,是否会锁住表?)。文件级别的缓存依赖,这个过程其实很简单了,在log4Net项目的作者自定义了实现了配置文件监控就用FileSystemWatcher,Timer结合使用就实现了log4net配置文件修改通知,这样你修改配置后不用重启服务器程序自动重新加载配置,其实用Timer就可以实现文件监控功能,定时(比如,x秒)获取文件修改时间并保留上次修改文件时间二者比较不一致那文件肯定别修改了;我认为,缓存依赖在数据变动(常见的配置开关)后程序执行逻辑又依赖这个配置参数,你希望不重启系统就能执行新的流程那么可以使用缓存依赖;如果数据过期就不应该使用缓存依赖,因为过期了下次你再获取这个缓存项是不会有数据的,那这时再次执行业务流程后数据放入缓存即可;数据级别缓存依赖需要慎重了,一切以程序稳定,性能为先,否则应该放弃这个功能。

    补充下对于缓存依赖,我们完全可以自定义实现(推模式,拉模式),在修改数据时预留接口,如果变化使用观察者模式很轻松就可以实现通知;定时后台线程轮询对象状态,如果变化则更新缓存数据。

    7.   HttpApplicationState

          1.   启用 ASP.NET 应用程序中多个会话和请求之间的全局信息共享——MSDN;第一个请求命中web服务时创建,并在应用程序关闭时释放——asp.net 2.0技术内幕。

         2.    Application.Lock(),Application.UnLock()应用程序状态变量可以同时被多个线程访问。因此,为了防止产生无效数据,在设置值前,必须锁定应用程序状态,只供一个线程写入; HttpApplicationState 类使用 AllKeys 和 Count 属性以及 Add、Clear、Get、GetKey、Remove、RemoveAt 和 Set 方法执行自动锁定和解锁。然而,当您有一系列操作时,显式地使用 Lock 和 UnLock 方法可能更加高效。——MSDN

    8. 集合 NET

    中提供了Queue,Stack,List<T>,HashTable<T>,Dictionary<K,V>,LinkedList<T>等集合,在使用这些集合前,我建议你:

           1.   根据业务需要来选择适合的集合,即这些集合主要能解决的问题及他们的优缺点

           2.   看看MSDN对这些集合的用途描述以及这些集合add,Search,的复杂度(大O符号的表述)

           3.   是否线程安全的,比如Dictionary就不线程安全,使用这个集合在多线程情况还需要我们包装下

           4.   对于全局集合最好生命静态的并给集合一个初始值大小,细心的朋友应该注意到如Dictionary,Queue之类的集合在系统默认的大小范围类Add复杂度 O(1),但超过这个大小的时候Add复杂是O(N)n是集合的大小,所以我建议你最好能给你的集合设置个初始化大小。

           5.   在多线程访问数据时NET4.0我们可以使用System.Collections.Concurrent命名空间下的基于线程安全的集合类型,否则我们需要自己包装非线程安全的结合使之适合多线程访问,其中lock,System.Threading.ReaderWriterLockSlim 这个类,以及System.Threading.Interlocked都是常用的类型

     

     第三部分 分布式缓存

    分布式架构在现在有很多成熟的框架及案例了,这是个必须熟悉的技术范畴,而分布式缓存只是其中一部分,其重要性不言而喻,第二个分析“NET缓存”其实是用分布式缓存(除asp.net会话系统部分,这部分我没尝试过,默认NET机制)都是可以实现的(我的项目中现在绝大部分都是使用分布式缓存),且在你正确使用的前提下其性能非常的好。下面的分布式缓存我只总结memcached,redis,kt 在使用时的注意事项,及使用场景分析,其本身使用我给出官方链接(官方的更全面,权威)。

    1.   Memcached

         1.1.  简介

                1.1.1    服务端目前版本 v1.4.15,我的项目服务端使用v1.4.13,客户端使用Enyim.Caching 这个API公司内部修改过所以版本不匹配官方的版本。

                 1.1.2    已经支持二进制协议了,如果你版本不支持二进制协议那就升级吧

         1.2.  注意事项

                 2.1   程序启动时最好设置-m的大小、-c设置最大并发数量(启动参数可以参考官方文档),key大小默认250个字符,Memcached每个value大小限制为1M,每个key默认过期时间是30天

         1.3.   使用场景

                13.1   Memcached提供了add,Set,Remove,append等操作也就是你的业务结构不需要排序,交集,并集诸如此类复杂的操作且value不超过1M那就使用Memcached吧

         1.4.    其他

              1.4. 1     Memcached服务端并集群,集群算法在客户端算法核心依据“一致性hash”,如果你不熟悉这个算法看看.net版的Memcached客户端还是值得的包括客户端连接池技术应用

              1.4.2     Memcached团队现在推出了Membase分布式存储,可以关注下。

              1.4.3     其他注意事项,请参阅上文的“使用规划”

              1.4.4   Memcached参考:

                            http://memcached.org/

                            http://code.google.com/p/memcached/wiki

                            http://tech.idv2.com/2008/07/10/memcached-001/

    2.  Redis

         2.1简介

                 1)     服务端最新版2.6.7,客户端官方推荐使用ServiceStack.Redis,我自己的项目也是使用的这个版本。

                 2)     Redis是个很神奇的的东西,读写性能不用说,其功能性更是远远多过memcached。

         2.2 注意事项

                1)     值得关注的是redis现在这个版本到底要不要用持久化存储这也是网上争论的话题,就我自己的项目来说也是把redis作为内存存储来用的。

                2)     Redis 目前版本集群还不成熟服务端master-slave模式容易出现单点故障(也没看到解决这个问题的文章),我自己把它部署成服务端单个服务在客户端做集群,这样使用起来也不错。(官方2.8版本推出集群,期待吧)

         2.3 使用场景

                1)     Redis是为关系性数据分布式存储而生的,如果你的缓存设计很多关系运行,及排序操作那么果断使用redis吧

          2.4 其他

                1)      新浪是国内使用redis的大客户关注下他们博客(写这个文章的时候我无法打开google所以不贴地址了),你应该能学到很实用性的东西。

                 2)     Redis使用参考

                             http://redis.io/

                             http://blog.nosqlfan.com/html/3537.html

                             http://www.cnblogs.com/lovecindywang/archive/2011/03/03/1969633.html

    3.  KT——Kyoto Tycoon、Kyotocabinet

           3.1  简介

                   1)     “Kyotocabinet Kyoto Cabinet runs very fast. For example, elapsed time to store one million records is 0.9 seconds for hash database, and 1.1 seconds for B+ tree database. Moreover, the size of database is very small. For example, overhead for a record is 16 bytes for hash database, and 4 bytes for B+ tree database. Furthermore, scalability of Kyoto Cabinet is great. The database size can be up to 8EB (9.22e18 bytes)“——KC官网。这性能看起来牛b,用起来确实不错,我的AMS系统中用kt来存储登录前浏览过的数据读写一直都很稳定的,cpu基本维持在10左右。

                   2)     Kt支持双主从双写模式,这一点很好避免了一个节点写入失败的而无法写入的情况。

                   3)     Kt内部包装了Kyotocabinet,其性能非常可观,另外kt兼容memcached协议所以你用memcached客户端协议即可对kt进行操作。

          3.2 注意事项

             1)     Kt启动要注意的参数

    ü  -plsv:一个可插拔的服务器指定的共享库文件 ____重要____

    ü  -plex: 指定配置的热插拔服务器 ____重要____

    ü  -pldb :指定的共享库文件的一个可插入的数据库(指定插件库的动态链接库文件)。

    ü  -bnum :指定哈希表的桶数量。官方推荐是记录数的两倍或者更高。____重要____

    ü  -msize :指定内存映射区域大小(如:4G)。 ____重要____

    ü  -dfunit 设定一个值,当碎片数超过这个值系统就进行碎片整理。____重要____

            3.3 使用场景

                 1)     如果业务计算后端结果需要大量的访问量、持久化一段时间(比如,3天)且你的结果输了简单,单一的那么可以使kt

            3.4其他

                  1) Kt使用参考

                     http://fallabs.com/kyototycoon/spex.html

                     http://wayne173.iteye.com/blog/1484723

    顺便说下,这篇文章从我的word拷贝过来时却没有保存原来的格式,汗

  • 相关阅读:
    编程题#2: 魔兽世界之二:装备
    程序设计实习MOOC / 继承和派生——编程作业 第五周程序填空题1
    【转】C++动态创建二维数组,二维数组指针
    HDU-2571命运
    HDU-1203 I NEED A OFFER!
    HDU-1003 Max Sum
    HDU2196-Computer
    HDU-1520 Anniversary party
    ChineseHelper(获取汉字字符串的首拼)
    车牌号正则表达式(新能源车牌)
  • 原文地址:https://www.cnblogs.com/liguo/p/2840769.html
Copyright © 2020-2023  润新知