简介:本文是一个简单的demo用于展示利用StackExchange.Redis和Log4Net构建日志队列,为高并发日志处理提供一些思路。

0、先下载安装Redis服务,然后再服务列表里启动服务(Redis的默认端口是6379,貌似还有一个故事)(https://github.com/MicrosoftArchive/redis/releases)

1、nuget中安装Redis:Install-Package StackExchange.Redis -version 1.2.6
2、nuget中安装日志:Install-Package Log4Net -version 2.0.8

3、创建RedisConnectionHelp、RedisHelper类,用于调用Redis。由于是Demo我不打算用完整类,比较完整的可以查阅其他博客(例如:https://www.cnblogs.com/liqingwen/p/6672452.html)

/// <summary>
/// StackExchange Redis ConnectionMultiplexer对象管理帮助类
/// </summary>
public class RedisConnectionHelp
{
//系统自定义Key前缀
public static readonly string SysCustomKey = ConfigurationManager.AppSettings["redisKey"] ?? "";
private static readonly string RedisConnectionString = ConfigurationManager.AppSettings["seRedis"] ?? "127.0.0.1:6379"; private static readonly object Locker = new object();
private static ConnectionMultiplexer _instance;
private static readonly ConcurrentDictionary<string, ConnectionMultiplexer> ConnectionCache = new ConcurrentDictionary<string, ConnectionMultiplexer>(); /// <summary>
/// 单例获取
/// </summary>
public static ConnectionMultiplexer Instance
{
get
{
if (_instance == null)
{
lock (Locker)
{
if (_instance == null || !_instance.IsConnected)
{
_instance = GetManager();
}
}
}
return _instance;
}
} /// <summary>
/// 缓存获取
/// </summary>
/// <param name="connectionString"></param>
/// <returns></returns>
public static ConnectionMultiplexer GetConnectionMultiplexer(string connectionString)
{
if (!ConnectionCache.ContainsKey(connectionString))
{
ConnectionCache[connectionString] = GetManager(connectionString);
}
return ConnectionCache[connectionString];
} private static ConnectionMultiplexer GetManager(string connectionString = null)
{
connectionString = connectionString ?? RedisConnectionString;
var connect = ConnectionMultiplexer.Connect(connectionString);
return connect;
}
}
public class RedisHelper
{
private int DbNum { get; set; }
private readonly ConnectionMultiplexer _conn;
public string CustomKey; public RedisHelper(int dbNum = )
: this(dbNum, null)
{
} public RedisHelper(int dbNum, string readWriteHosts)
{
DbNum = dbNum;
_conn =
string.IsNullOrWhiteSpace(readWriteHosts) ?
RedisConnectionHelp.Instance :
RedisConnectionHelp.GetConnectionMultiplexer(readWriteHosts);
} private string AddSysCustomKey(string oldKey)
{
var prefixKey = CustomKey ?? RedisConnectionHelp.SysCustomKey;
return prefixKey + oldKey;
} private T Do<T>(Func<IDatabase, T> func)
{
var database = _conn.GetDatabase(DbNum);
return func(database);
} private string ConvertJson<T>(T value)
{
string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value);
return result;
} private T ConvertObj<T>(RedisValue value)
{
Type t = typeof(T);
if (t.Name == "String")
{
return (T)Convert.ChangeType(value, typeof(string));
} return JsonConvert.DeserializeObject<T>(value);
} private List<T> ConvetList<T>(RedisValue[] values)
{
List<T> result = new List<T>();
foreach (var item in values)
{
var model = ConvertObj<T>(item);
result.Add(model);
}
return result;
} private RedisKey[] ConvertRedisKeys(List<string> redisKeys)
{
return redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray();
} /// <summary>
/// 入队
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void ListRightPush<T>(string key, T value)
{
key = AddSysCustomKey(key);
Do(db => db.ListRightPush(key, ConvertJson(value)));
} /// <summary>
/// 出队
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
public T ListLeftPop<T>(string key)
{
key = AddSysCustomKey(key);
return Do(db =>
{
var value = db.ListLeftPop(key);
return ConvertObj<T>(value);
});
} /// <summary>
/// 获取集合中的数量
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public long ListLength(string key)
{
key = AddSysCustomKey(key);
return Do(redis => redis.ListLength(key));
} }

4、创建log4net的配置文件log4net.config。设置属性为:始终复制、内容。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections> <log4net>
<root>
<!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) -->
<!--级别按以上顺序,如果level选择error,那么程序中即便调用info,也不会记录日志-->
<level value="ALL" />
<!--appender-ref可以理解为某种具体的日志保存规则,包括生成的方式、命名方式、展示方式-->
<appender-ref ref="MyErrorAppender"/>
</root> <appender name="MyErrorAppender" type="log4net.Appender.RollingFileAppender">
<!--日志路径,相对于项目根目录-->
<param name= "File" value= "Log\\"/>
<!--是否是向文件中追加日志-->
<param name= "AppendToFile" value= "true"/>
<!--日志根据日期滚动-->
<param name= "RollingStyle" value= "Date"/>
<!--日志文件名格式为:日期文件夹/Error_2019_3_19.log,前面的yyyyMMdd/是指定文件夹名称-->
<param name= "DatePattern" value= "yyyyMMdd/Error_yyyy_MM_dd&quot;.log&quot;"/>
<!--日志文件名是否是固定不变的-->
<param name= "StaticLogFileName" value= "false"/>
<!--日志文件大小,可以使用"KB", "MB" 或 "GB"为单位-->
<!--<param name="MaxFileSize" value="500MB" />-->
<layout type="log4net.Layout.PatternLayout,log4net">
<!--%n 回车-->
<!--%d 当前语句运行的时刻,格式%date{yyyy-MM-dd HH:mm:ss,fff}-->
<!--%t 引发日志事件的线程,如果没有线程名就使用线程号-->
<!--%p 日志的当前优先级别-->
<!--%c 当前日志对象的名称-->
<!--%m 输出的日志消息-->
<!--%-数字 表示该项的最小长度,如果不够,则用空格 -->
<param name="ConversionPattern" value="========[Begin]========%n%d [线程%t] %-5p %c 日志正文如下- %n%m%n%n" />
</layout>
<!-- 最小锁定模型,可以避免名字重叠。文件锁类型,RollingFileAppender本身不是线程安全的,-->
<!-- 如果在程序中没有进行线程安全的限制,可以在这里进行配置,确保写入时的安全。-->
<!-- 文件锁定的模式,官方文档上他有三个可选值“FileAppender.ExclusiveLock, FileAppender.MinimalLock and FileAppender.InterProcessLock”,-->
<!-- 默认是第一个值,排他锁定,一次值能有一个进程访问文件,close后另外一个进程才可以访问;第二个是最小锁定模式,允许多个进程可以同时写入一个文件;第三个目前还不知道有什么作用-->
<!-- 里面为什么是一个“+”号。。。问得好!我查了很久文件也不知道为什么不是点,而是加号。反正必须是加号-->
<param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" /> <!--日志过滤器,配置可以参考其他人博文:https://www.cnblogs.com/cxd4321/archive/2012/07/14/2591142.html -->
<filter type="log4net.Filter.LevelMatchFilter">
<LevelToMatch value="ERROR" />
</filter>
<!-- 上面的过滤器,其实可以写得很复杂,而且可以多个以or的形式并存。如果符合过滤条件就会写入日志,如果不符合条件呢?不是不要了-->
<!-- 相反是不符合过滤条件也写入日志,所以最后加一个DenyAllFilter,使得不符合上面条件的直接否决通过-->
<filter type="log4net.Filter.DenyAllFilter" />
</appender>
</log4net>
</configuration>

5、创建日志类LoggerFunc、日志工厂类LoggerFactory

/// <summary>
/// 日志单例工厂
/// </summary>
public class LoggerFactory
{
public static string CommonQueueName = "DisSunQueue";
private static LoggerFunc log;
private static object logKey = new object();
public static LoggerFunc CreateLoggerInstance()
{
if (log != null)
{
return log;
} lock (logKey)
{
if (log == null)
{
string log4NetPath = AppDomain.CurrentDomain.BaseDirectory + "Config\\log4net.config";
log = new LoggerFunc();
log.logCfg = new FileInfo(log4NetPath);
log.errorLogger = log4net.LogManager.GetLogger("MyError");
log.QueueName = CommonQueueName;//存储在Redis中的键名
log4net.Config.XmlConfigurator.ConfigureAndWatch(log.logCfg); //加载日志配置文件S
}
} return log;
}
}
/// <summary>
/// 日志类实体
/// </summary>
public class LoggerFunc
{
public FileInfo logCfg;
public log4net.ILog errorLogger;
public string QueueName; /// <summary>
/// 保存错误日志
/// </summary>
/// <param name="title">日志内容</param>
public void SaveErrorLogTxT(string title)
{
RedisHelper redis = new RedisHelper();
//塞进队列的右边,表示从队列的尾部插入。
redis.ListRightPush<string>(QueueName, title);
} /// <summary>
/// 日志队列是否为空
/// </summary>
/// <returns></returns>
public bool IsEmptyLogQueue()
{
RedisHelper redis = new RedisHelper();
if (redis.ListLength(QueueName) > )
{
return false;
}
return true;
} }

6、创建本章最核心的日志队列设置类LogQueueConfig。

ThreadPool是线程池,通过这种方式可以减少线程的创建与销毁,提高性能。也就是说每次需要用到线程时,线程池都会自动安排一个还没有销毁的空闲线程,不至于每次用完都销毁,或者每次需要都重新创建。但其实我不太明白他的底层运行原理,在内部while,是让这个线程一直不被销毁一直存在么?还是说sleep结束后,可以直接拿到一个线程池提供的新线程。为什么不是在ThreadPool.QueueUserWorkItem之外进行循环调用?了解的童鞋可以给我留下言。

/// <summary>
/// 日志队列设置类
/// </summary>
public class LogQueueConfig
{
public static void RegisterLogQueue()
{
ThreadPool.QueueUserWorkItem(o =>
{
while (true)
{
RedisHelper redis = new RedisHelper();
LoggerFunc logFunc = LoggerFactory.CreateLoggerInstance();
if (!logFunc.IsEmptyLogQueue())
{
//从队列的左边弹出,表示从队列头部出队
string logMsg = redis.ListLeftPop<string>(logFunc.QueueName); if (!string.IsNullOrWhiteSpace(logMsg))
{
logFunc.errorLogger.Error(logMsg);
}
}
else
{
Thread.Sleep(); //为避免CPU空转,在队列为空时休息1秒
}
}
});
}
}

7、在项目的Global.asax文件中,启动队列线程。本demo由于是在winForm中,所以放在form中。

        public Form1()
{
InitializeComponent();
RedisLogQueueTest.CommonFunc.LogQueueConfig.RegisterLogQueue();//启动日志队列
}

8、调用日志类LoggerFunc.SaveErrorLogTxT(),插入日志。

            LoggerFunc log = LoggerFactory.CreateLoggerInstance();
log.SaveErrorLogTxT("您插入了一条随机数:"+longStr);

9、查看下入效果

10、完整源码(winForm不懂?差不多的啦,打开项目直接运行就可以看见界面):

https://gitee.com/dissun/RedisLogQueueTest

#### 原创:DisSun ##########

#### 时间:2019.03.19 #######

利用StackExchange.Redis和Log4Net构建日志队列的更多相关文章

  1. StackExchange.Redis和Log4Net构建日志

    利用StackExchange.Redis和Log4Net构建日志队列   简介:本文是一个简单的demo用于展示利用StackExchange.Redis和Log4Net构建日志队列,为高并发日志处 ...

  2. .NetCore使用Redis,StackExchange.Redis队列,发布与订阅,分布式锁的简单使用

    环境:之前一直是使用serverStack.Redis的客服端,今天来使用一下StackExchange.Redis(个人感觉更加的人性化一些,也是免费的,性能也不会差太多),版本为StackExch ...

  3. linux下利用elk+redis 搭建日志分析平台教程

    linux下利用elk+redis 搭建日志分析平台教程 http://www.alliedjeep.com/18084.htm   elk 日志分析+redis数据库可以创建一个不错的日志分析平台了 ...

  4. JavaWeb项目架构之Redis分布式日志队列

    架构.分布式.日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个Redis做消息队列罢了. 前言 为什么需要消息队列? 当系统中出现"生产"和" ...

  5. C# 利用Log4Net进行日志记录

    概述 本文主要简单说明如何使用Log4Net进行日志记录,在程序开发过程中记录日志的优点: 它可以提供应用程序运行时的精确环境,可供开发人员尽快找到应用程序中的Bug: 一旦在程序中加入了Log 输出 ...

  6. c# redis 利用锁(StackExchange.Redis LockTake)来保证数据在高并发情况下的正确性

    之前有写过一篇介绍c#操作redis的文章 http://www.cnblogs.com/axel10/p/8459434.html ,这篇文章中的案例使用了StringIncrement来实现了高并 ...

  7. 利用log4net创建日志文件时过滤日志,这是坑还是?

    前言 网上貌似没有太多关于log4net过滤日志的资料,在研究过程中发现一点小问题,这里做下记录,希望对后续有用到的童鞋起到一丢丢帮助作用. log4net日志过滤 由于是在.NET Core中使用, ...

  8. ELK+kafka构建日志收集系统

    ELK+kafka构建日志收集系统   原文  http://lx.wxqrcode.com/index.php/post/101.html   背景: 最近线上上了ELK,但是只用了一台Redis在 ...

  9. Lind.DDD.RedisClient~对StackExchange.Redis调用者的封装及多路复用技术

    回到目录 两雄争霸 使用StackExchange.Redis的原因是因为它开源,免费,而对于商业化的ServiceStack.Redis,它将一步步被前者取代,开源将是一种趋势,商业化也值得被我们尊 ...

随机推荐

  1. 将texlive带的字体安装进linux系统字体库

    装机之后装系统,装完系统就装texlive,然后又遇一坑,编译以前的文档竟然找不到某字体: kpathsea:make_tex: Invalid fontname `FontAwesome Regul ...

  2. sublime text3简体中文版汉化教程

    今天突然想到好像还有一个强大的编译器sublime text 3可是这个是外国的编译器,不过各位不用担心 这个编译器,已经支持中文编译了: 下面就是我关于汉化为中文方面的一些了解以及汉化方式(由于我的 ...

  3. ArchLinux 安装五笔输入法

    说明 自己的笔记本已经全盘做成了ArchLinux系统了,用着还好,苦于常用的五笔输入法在Arch下有点不太好装,参考wiki弄好了,这里简单记录下 这里使用ibus-rime 原因有二: ibus- ...

  4. 为啥程序会有bug?

    如果这是第二次看到我的文章,欢迎右侧扫码订阅我哟~ 

  5. ceph 常见问题百科全书---luminous安装部署篇

    1. 执行步骤:ceph-deploy new node        机器:centos 7.5   ceph  Luminous版本     源:阿里云 问题: Traceback (most r ...

  6. 广州站长沙龙 MIP 问题及答案

    1. mip提交几个月时间了,生效量比较少,是什么原因? 答:提交 MIP 页面后,经过收录.校验.和生效三个步骤,才能在结果页看到闪电标. 1)提交 URL 后,spider 会去抓取收录: 2)页 ...

  7. 微软开源自动机器学习工具NNI安装与使用

    微软开源自动机器学习工具 – NNI安装与使用   在机器学习建模时,除了准备数据,最耗时耗力的就是尝试各种超参组合,找到最佳模型的过程了.对于初学者来说,常常是无从下手.即使是对于有经验的算法工程师 ...

  8. .NET 反编译调试神器:dnSpy了解一下

    如果客户环境出了问题,而又无法快速定位问题,可以借助dnSpy进行反编译调试跟踪. 可前往dnSpy官网下载或直接从我的分享链接下载(内置包含.NET Framework 4.7.1,若运行提示需要安 ...

  9. Java GC性能优化实战

    GC优化是必要的吗? 或者更准确地说,GC优化对Java基础服务来说是必要的吗?答案是否定的,事实上GC优化对Java基础服务来说在有些场合是可以省去的,但前提是这些正在运行的Java系统,必须包含以 ...

  10. 直观理解神经网络最后一层全连接+Softmax

    目录 写在前面 全连接层与Softmax回顾 加权角度 模板匹配 几何角度 Softmax的作用 总结 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 这篇文章将从3 ...