分享

文件并发(日志处理)--队列--Redis Log4Net - 邹琼俊 - 博客园

 Lenic 2015-04-09

Redis简介

Redis是一个开源的,使用C语言编写,面向“键/值”对类型数据的分布式NoSQL数据库系统,特点是高性能,持久存储,适应高并发的应用场景。Redis纯粹为应用而产生,它是一个高性能的key-value数据库,并且提供了多种语言的API

性能测试结果表示SET操作每秒钟可达110000次,GET操作每秒81000次(当然不同的服务器配置性能不同)。

redis目前提供五种数据类型:string(字符串),list(链表), Hash(哈希),set(集合)及zset(sorted set)  (有序集合)

RedisMemcached的比较.

  1. Memcached是多线程,而Redis使用单线程.
  2. Memcached使用预分配的内存池的方式,Redis使用现场申请内存的方式来存储数据,并且可以配置虚拟内存。
  3. Redis可以实现持久化,主从复制,实现故障恢复。
  4. Memcached只是简单的keyvalue,但是Redis支持数据类型比较多。

Redis的存储分为内存存储、磁盘存储 .从这一点,也说明了RedisMemcached是有区别的。Redis Memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改 操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis有两种存储方式,默认是snapshot方式,实现方法是定时将内存的快照(snapshot)持久化到硬盘,这种方法缺点是持久化之后如果出现crash则会丢失一段数据。因此在完美主义者的推动下作者增加了aof方式。aofappend only mode,在写入内存数据的同时将操作命令保存到日志文件,在一个并发更改上万的系统中,命令日志是一个非常庞大的数据,管理维护成本非常高,恢复重建时间会非常长,这样导致失去aof高可用性本意。另外更重要的是Redis是一个内存数据结构模型,所有的优势都是建立在对内存复杂数据结构高效的原子操作上,这样就看出aof是一个非常不协调的部分。

其实aof目的主要是数据可靠性及高可用性.

Redis安装

下载Redis:https://github.com/dmajkic/redis/downloads   (如果点击后出现404错误,请按F5刷新下界面即可)

将服务程序拷贝到一个磁盘上的目录,如下图:

文件说明:

  • redis-server.exe:服务程序  
  • redis-check-dump.exe:本地数据库检查 
  • redis-check-aof.exe:更新日志检查 
  • redis-benchmark.exe:性能测试,用以模拟同时由N个客户端发送M个 SETs/GETs 查询.
  • redis-cli.exe: 服务端开启后,我们的客户端就可以输入各种命令测试了

1、打开一个cmd窗口,使用cd命令切换到指定目录(F:\Redis)运行 redis-server.exe redis.conf 

2、重新打开一个cmd窗口,使用cd命令切换到指定目录(F:\Redis)运行 redis-cli.exe -h 127.0.0.1 -p 6379,其中 127.0.0.1是本地ip6379redis服务端的默认端口 (这样可以开启一个客户端程序进行特殊指令的测试).

可以将此服务设置为windows系统服务,下载Redis服务安装软件,安装即可。(https://github.com/rgl/redis/downloads 

安装完成Redis服务后,我们会在计算机的服务里面看到

然后启动此服务。

接下来在使用Redis时,还需要下载C#驱动(也就是C#开发库),如下图:

Redis常用数据类型

使用Redis,我们不用在面对功能单调的数据库时,把精力放在如何把大象放进冰箱这样的问题上,而是利用Redis灵活多变的数据结构和数据操作,为不同的大象构建不同的冰箱。

 Redis最为常用的数据类型主要有以下五种: 

  •    String
  •    Hash
  •    List
  •    Set
  •    Sorted set

String类型

String是最常用的一种数据类型,普通的key/value存储都可以归为此类 。一个Key对应一个Value,string类型是二进制安全的。Redisstring可以包含任何数据,比如jpg图片(生成二进制)或者序列化的对象。基本操作如下:

  1.  var client = new RedisClient('127.0.0.1', 6379);  
  2.             client.Set<int>('pwd', 1111);  
  3.             int pwd=client.Get<int>('pwd');  
  4.             Console.WriteLine(pwd);  
  5.    
  6. UserInfo userInfo = new UserInfo() { UserName = 'zhangsan', UserPwd = '1111' };<span style='font-family:宋体;'>//</span>(底层使用json序列化 )  
  7. client.Set<UserInfo>('userInfo', userInfo);  
  8. UserInfo user=client.Get<UserInfo>('userInfo');  
  9. Console.WriteLine(user.UserName);  
  10.   
  11. List<UserInfo> list = new List<UserInfo>() { new UserInfo(){UserName='lisi',UserPwd='111'},new UserInfo(){UserName='wangwu',UserPwd='123'} };  
  12. client.Set<List<UserInfo>>('list',list);  
  13. List<UserInfo>userInfoList=client.Get<List<UserInfo>>('list');  
  14.   
  15.  foreach (UserInfo userInfo in userInfoList)  
  16.             {  
  17.                 Console.WriteLine(userInfo.UserName);  
  18.             }  

Hash类型

Hash是一个string 类型的fieldvalue的映射表。hash特别适合存储对象。相对于将对象的每个字段存成单个string 类型。一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。

作为一个key value存在,很多开发者自然的使用set/get方式来使用Redis,实际上这并不是最优化的使用方法。尤其在未启用VM情况下,Redis全部数据需要放入内存,节约内存尤其重要.

增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回 

Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争 .. redis是个单线程的程序,为什么会这么快呢 ?

  1. 大量线程导致的线程切换开销
  2. 锁、
  3. 非必要的内存拷贝。 
  4. Redis多样的数据结构,每种结构只做自己爱做的事.

Hash对应的Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个HashMap的成员比较少时,Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,当成员量增大时会自动转成真正的HashMap. 

Key仍然是用户ID, value是一个Map,这个Mapkey是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部MapKey(Redis里称内部Mapkeyfield), 也就是通过 key(用户ID) + field(属性标签就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和反序列化

  1. client.SetEntryInHash('user', 'userInfo', 'aaaaaaaaaa');  
  2. List<string> list = client.GetHashKeys('user');  
  3. List<string> list = client.GetHashValues('userInfo');//获取值  
  4. List<string> list = client.GetAllKeys();//获取所有的key。  

Redis为不同数据类型分别提供了一组参数来控制内存使用,我们在前面提到过的Redis Hashvalue内部是一个

HashMap,如果该Map的成员比较少,则会采用一维数组的方式来紧凑存储该MAP,省去了大量指针的内存开销,这个参数在redis,conf配置文件中下面2项。

Hash-max-zipmap-entries 64

Hash-max-zipmap-value 512.

含义是当value这个Map内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即value内部有64个以下的成员就是使用线性紧凑存储,超过该值自动转成真正的HashMap.

Hash-max-zipmap-value含义是当value这个MAP内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。以上两个条件任意一个条件超过设置值都会转成真正的HashMap,也就不会再节省内存了,这个值设置多少需要权衡,HashMap的优势就是查找和操作时间短。

一个key可对应多个field,一个field对应一个value   

这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意 

建议使用对象类别和ID构成键名,使用字段表示对象属性,字

段值存储属性值,例如:car:2 price 500

List类型

list是一个链表结构,主要功能是push,pop,获取一个范围的所有的值等,操作中key理解为链表名字。 Redislist类型其实就是一个每个子元素都是string类型的双向链表。我们可以通过push,pop操作从链表的头部或者尾部添加删除元素,这样list既可以作为栈,又可以作为队列。Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构

  1. //队列使用  
  2.   
  3.  client.EnqueueItemOnList('name', 'zhangsan');  
  4.            client.EnqueueItemOnList('name', 'lisi');  
  5.            int count= client.GetListCount('name');  
  6.            for (int i = 0; i < count; i++)  
  7.            {  
  8.               Console.WriteLine(client.DequeueItemFromList('name'));  
  9.            }  
  10.   
  11. //栈使用  
  12.             client.PushItemToList('name2', 'wangwu');  
  13.             client.PushItemToList('name2', 'maliu');  
  14.             int count = client.GetListCount('name2');  
  15.             for (int i = 0; i < count; i++)  
  16.             {  
  17.                 Console.WriteLine(client.PopItemFromList('name2'));  
  18.             }  

Set类型

它是string类型的无序集合。set是通过hash table实现的,添加,删除和查找,对集合我们可以取并集,交集,差集.

  1. //对Set类型进行操作  
  2.             client.AddItemToSet('a3', 'ddd');  
  3.             client.AddItemToSet('a3', 'ccc');  
  4.             client.AddItemToSet('a3', 'tttt');  
  5.             client.AddItemToSet('a3', 'sssh');  
  6.             client.AddItemToSet('a3', 'hhhh');  
  7.             System.Collections.Generic.HashSet<string>hashset=client.GetAllItemsFromSet('a3');  
  8.             foreach (string str in hashset)  
  9.             {  
  10.                 Console.WriteLine(str);  
  11.             }  
  12.   
  13. //求并集  
  14. client.AddItemToSet('a3', 'ddd');  
  15.             client.AddItemToSet('a3', 'ccc');  
  16.             client.AddItemToSet('a3', 'tttt');  
  17.             client.AddItemToSet('a3', 'sssh');  
  18.             client.AddItemToSet('a3', 'hhhh');  
  19.             client.AddItemToSet('a4', 'hhhh');  
  20.             client.AddItemToSet('a4', 'h777');  
  21.  System.Collections.Generic.HashSet<string>hashset=  client.GetUnionFromSets(new string[] { 'a3','a4'});  
  22.   
  23.           foreach (string str in hashset)  
  24.           {  
  25.               Console.WriteLine(str);  
  26.           }  
  27.   
  28. //求交集  
  29.  System.Collections.Generic.HashSet<string> hashset = client.GetIntersectFromSets(new string[] { “a3”, “a4” });  
  30.   
  31. //求差集.  
  32.  System.Collections.Generic.HashSet<string> hashset = client.GetDifferencesFromSet('a3',new string[] { 'a4'});  

返回存在于第一个集合,但是不存在于其他集合的数据。差集

Sorted Set类型

sorted set set的一个升级版本,它在set的基础上增加了一个顺序的属性,这一属性在添加修改   .元素的时候可以指定,每次指定后,zset(表示有序集合)会自动重新按新的值调整顺序。可以理解为有列的表,一列存 value,一列存顺序。操作中key理解为zset的名字.

Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构, 

  1. client.AddItemToSortedSet('a5', 'ffff');  
  2.            client.AddItemToSortedSet('a5','bbbb');  
  3.            client.AddItemToSortedSet('a5', 'gggg');  
  4.            client.AddItemToSortedSet('a5', 'cccc');  
  5.            client.AddItemToSortedSet('a5', 'waaa');  
  6.            System.Collections.Generic.List<string> list =client.GetAllItemsFromSortedSet('a5');  
  7.            foreach (string str in list)  
  8.            {  
  9.                Console.WriteLine(str);  
  10.            }  

Redis工具和所需资料代码全下载,地址:http://pan.baidu.com/s/1c0ChL5m

多线程操作同一个文件时会出现并发问题。解决的一个办法就是给文件加锁(lock),但是这样的话,一个线程操作文件时,其它的都得等待,这样的话性能非常差。另外一个解决方案,就是先将数据放在队列中,然后开启一个线程,负责从队列中取出数据,再写到文件中。

下面我们讲解一个实际项目中应用的案例,关于日志的处理.这里是使用ASP.NET MVC项目作为Demo

方式一:使用队列

思路:把所有产生的日志信息存放到一个队列里面,然后通过新建一个线程,不断的从这个队列里面读取异常信息,然后往日志里面写。也就是所谓的生产者、消费者模式。

1、新建一个类MyErrorAttribute,

  1. using System.Web.Mvc;  
  2.     public class MyErrorAttribute : HandleErrorAttribute  
  3.     {  
  4.         public static Queue<Exception> ExceptionQueue = new Queue<Exception>();  
  5.         public override void OnException(ExceptionContext filterContext)  
  6.         {  
  7.             ExceptionQueue.Enqueue(filterContext.Exception);  
  8.             filterContext.HttpContext.Response.Redirect('~/Error.html');  
  9.             base.OnException(filterContext);  
  10.         }  
  11. }  

2、在FilterConfig类中进行如下修改:

  1. public class FilterConfig  
  2.   {  
  3.       public static void RegisterGlobalFilters(GlobalFilterCollection filters)  
  4.       {  
  5.           //filters.Add(new HandleErrorAttribute());  
  6.   
  7.           filters.Add(new MyErrorAttribute());  
  8.       }  
3、在Gobal.asax.cs中的Application_Start事件里添加如下代码:
  1. string filePath = Server.MapPath('~/Logs/');  
  2.            ThreadPool.QueueUserWorkItem(o => {   
  3.            while(true)  
  4.            {  
  5.                if (MyErrorAttribute.ExceptionQueue.Count > 0)  
  6.                {  
  7.                    Exception ex = MyErrorAttribute.ExceptionQueue.Dequeue();  
  8.                    if (ex != null)  
  9.                    {  
  10.                        string fileName = filePath + DateTime.Now.ToString('yyyy-MM-dd') + '.txt';  
  11.                        File.AppendAllText(fileName, ex.Message);  
  12.                    }  
  13.                    else  
  14.                    {  
  15.                        Thread.Sleep(50);  
  16.                    }  
  17.                }  
  18.                else  
  19.                {  
  20.                    Thread.Sleep(50);  
  21.                }  
  22.            }  
  23.            });  

方式二:使用RedisLog4Net完成分布式日志记录

Log4Net是用来记录日志的,可以将程序运行过程中的信息输出到一些地方(文件、数据库、EventLog等),日志就是程序的黑匣子,可以通过

日志查看系统的运行过程,从而发现系统的问题。日志的作用:将运行过程的步骤、成功失败记录下来,将关键性的数据记录下来分析系统问题所在。

对于网站来讲,不能把异常信息显示给用户,异常信息只能记录到日志,出了问题把日志文件发给开发人员,就能知道问题所在。

配置Log4Net环境

(1)新建一个WebApplication

(2)添加对log4net.dll的引用(bin\net\2.0\release 不能引用debug版本)(把相应的dll文件拷贝到项目中的lib文件夹下。)

(3)Web.Config (或App.Config)添加配置

(4)初始化:在程序最开始加入log4net.Config.XmlConfigurator.Configure()

在要打印日志的地方LogManager.GetLogger(typeof(Program)).Debug(“信息”); 。通过LogManager.GetLogger传递要记录的日志类类名获得这个类的ILog(这样在日志文件中就能看到这条日志是哪个类输出的了),然后调用Debug方法输出消息。因为一个类内部不止一个地方要打印日志,所以一般把ILog声明为一个static字段。

Private static ILog logger=LogManager.GetLogger(typeof(Test))

输出错误信息用ILog.Error方法,第二个参数可以传递Exception对象。log.Error('***错误'+ex)log.Error('***错误',ex)

Appender:可以将日志输出到不同的地方,不同的输出目标对应不同的AppenderRollingFileAppender(滚动文件)、AdoNetAppender(数据库)、SmtpAppender (邮件)等。

level(级别):标识这条日志信息的重要级别None>Fatal>ERROR>WARN>DEBUG>INFO>ALL,设定一个

Level,那么低于这个Level的日志是不会被写到Appender中的.

Log4Net还可以设定多个Appender,可以实现同时将日志记录到文件、数据、发送邮件等;可以设定不同的Appender的不同的Level,可以实现普通级别都记录到文件,Error以上级别发送邮件;可以实现对不同的类设定不同的Appender;还可以自定义Appender,这样可以自己实现将Error信息发短信等.

示例:

1、配置Log4Net,在Web.config中添加如下配置:

  1. <configSections>  
  2.     <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->  
  3.     <section name='entityFramework' type='System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0,   
  4. Culture=neutral, PublicKeyToken=b77a5c561934e089' requirePermission='false' />  
  5.     <section name='log4net' type='log4net.Config.Log4NetConfigurationSectionHandler, log4net'/>  
  6.   </configSections>  
  7.   <log4net>  
  8.     <!-- OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL -->  
  9.     <!-- Set root logger level to ERROR and its appenders -->  
  10.     <root>  
  11.       <level value='ALL'/>  
  12.       <appender-ref ref='SysAppender'/>  
  13.     </root>  
  14.     <!-- Print only messages of level DEBUG or above in the packages -->  
  15.     <logger name='WebLogger'>  
  16.       <level value='DEBUG'/>  
  17.     </logger>  
  18.     <appender name='SysAppender' type='log4net.Appender.RollingFileAppender,log4net' >  
  19.       <param name='File' value='App_Data/' />  
  20.       <param name='AppendToFile' value='true' />  
  21.       <param name='RollingStyle' value='Date' />  
  22.       <param name='DatePattern' value=''Logs_'yyyyMMdd'.txt'' />  
  23.       <param name='StaticLogFileName' value='false' />  
  24.       <layout type='log4net.Layout.PatternLayout,log4net'>  
  25.         <param name='ConversionPattern' value='%d [%t] %-5p %c - %m%n' />  
  26.         <param name='Header' value='----------------------header--------------------------' />  
  27.         <param name='Footer' value='----------------------footer--------------------------' />  
  28.       </layout>  
  29.     </appender>  
  30.     <appender name='consoleApp' type='log4net.Appender.ConsoleAppender,log4net'>  
  31.       <layout type='log4net.Layout.PatternLayout,log4net'>  
  32.         <param name='ConversionPattern' value='%d [%t] %-5p %c - %m%n' />  
  33.       </layout>  
  34.     </appender>  
  35.   </log4net>  

2、添加ServiceStack.dllServiceStack.Interfaces.dllServiceStack.ServiceInterface.dlllog4net.dll的引用,然后新建一个类MyErrorAttribute,

  1. using System.Web.Mvc;  
  2. using ServiceStack.Redis;  
  3.  public static IRedisClientsManager clientsManager = new PooledRedisClientManager(new string[] { '127.0.0.1:6379'});  
  4.         public static IRedisClient redisClient = clientsManager.GetClient();  
  5.   
  6.         public override void OnException(ExceptionContext filterContext)  
  7.         {  
  8.             redisClient.EnqueueItemOnList('errorMsg', filterContext.Exception.ToString());  
  9.             filterContext.HttpContext.Response.Redirect('~/Error.html');  
  10.             base.OnException(filterContext);  
  11.         }  

3、在FilterConfig类中进行如下修改:

  1.  public class FilterConfig  
  2.     {  
  3.         public static void RegisterGlobalFilters(GlobalFilterCollection filters)  
  4.         {  
  5.             //filters.Add(new HandleErrorAttribute());  
  6.   
  7.             //filters.Add(new MyErrorAttribute());  
  8.             filters.Add(new MyExceptionAttribute());  
  9.         }  
  10. }  

4、在Gobal.asax.cs中的Application_Start事件里添加如下代码:

  1. log4net.Config.XmlConfigurator.Configure(); //获取Log4Net配置信息  
  2.             ThreadPool.QueueUserWorkItem(o =>  
  3.             {  
  4.                 while (true)  
  5.                 {  
  6.                     if (MyExceptionAttribute.redisClient.GetListCount('errorMsg') > 0)  
  7.                     {  
  8.                         string msg = MyExceptionAttribute.redisClient.DequeueItemFromList('errorMsg');  
  9.                         if (!string.IsNullOrEmpty(msg))  
  10.                         {  
  11.                             ILog logger=LogManager.GetLogger('testError');  
  12.                             logger.Error(msg); //将异常信息写入Log4Net中  
  13.                         }  
  14.                         else  
  15.                         {  
  16.                             Thread.Sleep(50);  
  17.                         }  
  18.                     }  
  19.                     else  
  20.                     {  
  21.                         Thread.Sleep(50);  
  22.                     }  
  23.                 }  
  24.             }); 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多