很长一段时间以来,一直在项目中使用Redis作为辅助存储,确切来说是利用Redis的内存存储,而不是将其作为缓存。比如常见的利用Set集合来判断某个数值是否存在,或者将来自不同请求的数据放在Redis中进行拼接然后一起写入MySQL等数据库。

这种存储目的的使用要求对Redis的访问不能失败(如果作为缓存使用,是接受失败的),所以作为存储目的使用代码中要对请求Redis的代码进行异常处理以及重试等。

在最初的代码中采用了最常见的方法如try ... catch ...处理异常,递归进行重试,类似:

//伪代码
public void Process(int retry)
{
if(retry>3)
{
//记录错误
return;
}
try
{
//业务代码
}
catch(Exception ex)
{
//重试
++retry;
Process(retry);
}
}

后来有一天看到了园友Jeffcky推荐的Polly库,瞬间眼前一亮,这才是我们处理异常和重试所需要的东西。

关于Polly的使用,可以参考Jeffcky的博文或者Polly项目的GitHub主页(文档很详细)。

大致的代码结构如:

var tsArr = new TimeSpan[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)
};
// 构造一种重试测试(其它可选的包括熔断等)
var policy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(tsArr); // 需要有Polly调用的业务代码,以异步方法为例
async Task SomeToInvoke()
{
// 一些异步调用
} // 使用Polly执行业务代码(如不需要捕获异常可选用其它重载)
var pollyRet = await policy.ExecuteAndCaptureAsync(SomeToInvoke);
// 处理返回值判断调用是否成功,或发生了什么异常

下面一步步来看博主的实现过程。

先放上一些测试所用的代码,首先是创建Redis连接的接口和类,它们是从NopCommerce项目一个早起版本借(chao)鉴(xi)来的(文件名都没改,为了测试方便代码略有改动),一直用着没啥大问题就这样用了。

public interface IRedisConnectionWrapper : IDisposable
{
IDatabase Database(int? db = null);
IServer Server(EndPoint endPoint);
EndPoint[] GetEndpoints();
void FlushDb(int? db = null);
}
public class RedisConnectionWrapper : IRedisConnectionWrapper
{
private readonly Lazy<string> _connectionString;
private readonly Lazy<string> _auth; private volatile ConnectionMultiplexer _connection;
private readonly object _lock = new object(); public RedisConnectionWrapper(string server, string pswd)
{
this._connectionString = new Lazy<string>(() => server);
this._auth = new Lazy<string>(() => pswd);
} private ConnectionMultiplexer GetConnection()
{
if (_connection != null && _connection.IsConnected) return _connection; lock (_lock)
{
if (_connection != null && _connection.IsConnected) return _connection; if (_connection != null)
{
_connection.Dispose();
} var options = new ConfigurationOptions();
options.EndPoints.Add(_connectionString.Value);
if (!string.IsNullOrEmpty(_auth.Value))
options.Password = _auth.Value; _connection = ConnectionMultiplexer.Connect(options);
} return _connection;
} public IDatabase Database(int? db = null)
{
return GetConnection().GetDatabase(db ?? -1);
} public IServer Server(EndPoint endPoint)
{
return GetConnection().GetServer(endPoint);
} public EndPoint[] GetEndpoints()
{
return GetConnection().GetEndPoints();
} public void FlushDb(int? db = null)
{
var endPoints = GetEndpoints(); foreach (var endPoint in endPoints)
{
Server(endPoint).FlushDatabase(db ?? -1);
}
} public void Dispose()
{
if (_connection != null)
{
_connection.Dispose();
}
}
}

对于StackExchange.Redis来说是比较标准的连接创建方式,顺便看了下新版的NopCommerce代码中,代码有了些小改进,增加了一个双重锁。有需要的园友可以自行去下载新的。

接着开始考虑重试问题,为了代码看起来更简洁,决定尝试通过动态代理将捕捉异常并重试的操作作为切面注入。说到动态代理,第一个想到肯定是Castle.Core(前身为CastleDynamicProxy)。动态代理可以选择接口或者是类,如果是类的话需要方法是虚方法。看了下StackExchange.Redis的代码,几个实现类都是internal,方法也都是非virtual。所以只能只能自己写一个类包一下。

这个类就是一个壳,为了我们切面注入。下面的代码只保留的一个方法,其它的省略。另外Castle.Core的动态代理是不支持异步方法的,所以先用Redis的同步接口做下尝试。

public class RedisDatabaseWrapper:IDatabase
{
private IDatabase _redisDb; public RedisDatabaseWrapper(IRedisConnectionWrapper redisConnectionWrapper)
{
_redisDb = redisConnectionWrapper.Database();
} public virtual bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
{
return _redisDb.SetContains(key, value, flags);
} // 省略其它所有方法...
}

安装Castle.Core,并开始实现动态代理类。

public class RetryByPollyInterceptor : IInterceptor
{
public async void Intercept(IInvocation invocation)
{
var isAsync = IsAsyncMethod(invocation.Method);
if (isAsync)
InterceptAsync(invocation);
else
InterceptSync(invocation);
} private void InterceptSync(IInvocation invocation)
{
var tsArr = new TimeSpan[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)
}; Action<Exception, TimeSpan, int, Context> action = (ex, ts, idx, ctx) =>
{
Console.WriteLine($"Polly Exp:{ex.GetType()} {ex.Message} Try:{idx} "); var invca = (IInvocation)ctx["inv"];
if (idx == 2)
{
var type = invca.Method.ReturnType;
if (type == typeof(void)) return;
var ret = type.IsValueType ? Activator.CreateInstance(type) : null;
invca.ReturnValue = ret;
}
}; var policy = Policy
.Handle<TimeoutException>()
.Or<RedisConnectionException>()
.Or<Exception>()
.WaitAndRetry(tsArr, action); void OrignalInvoke()
{
invocation.Proceed();
} var pollyRet = policy.ExecuteAndCapture(OrignalInvoke, new Dictionary<string, object>() { ["inv"] = invocation });
if (pollyRet.Outcome != OutcomeType.Successful)
{
Console.WriteLine($"Polly Ret Type:{pollyRet.Outcome} Exp:{pollyRet.ExceptionType} Msg:{pollyRet.FinalException?.Message}");
}
} private void InterceptAsync(IInvocation invocation)
{
// 异步方法代理,下文会讨论
} private static bool IsAsyncMethod(MethodInfo method)
{
return (
method.ReturnType == typeof(Task) ||
(method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
);
}
}

注意

这个方法也是经过多次尝试才最终完成,可以看到这里预留了处理异步代理的方法,后文会详细说。对于同步方法这段代码可以完美的捕获异常并重试。不用在外侧代码进行catch。当然内部发生异常并多次重试仍失败后会返回非期望的结果,还是需要根据业务的需要对返回值进行判断。

这段代码最值得注意的是这几行:

Action<Exception, TimeSpan, int, Context> action = (ex, ts, idx, ctx) =>
{
Console.WriteLine($"Polly Exp:{ex.GetType()} {ex.Message} Try:{idx} "); var invca = (IInvocation)ctx["inv"];
if (idx == 2)
{
var type = invca.Method.ReturnType;
if (type == typeof(void)) return;
var ret = type.IsValueType ? Activator.CreateInstance(type) : null;
invca.ReturnValue = ret;
}
};

由于我们设置重试两次,当第二次发生异常时,我们强制给方法返回值赋一个返回值,这样可以让外部调用方法正常执行下去而不会由于无法获取代理方法的返回值而报空引用异常。

接着看看其它组成部分。在博主目前大部分项目中都使用Autofac作为容器,我们需要注册一下用到的类。并且通过Autofac的Castle.Core插件,可以注册动态代理,这样就不用通过给类添加Attribute的方式来添加代理,这是个人比较喜欢的风格。

var builder = new ContainerBuilder();

builder.Register(c => new RetryByPollyInterceptor()); //动态代理类

builder.RegisterType<RedisDatabaseWrapper>().As<IDatabase>().EnableInterfaceInterceptors().InterceptedBy(typeof(RetryByPollyInterceptor)).SingleInstance(); //添加动态代理

builder.RegisterType<RedisConnectionWrapper>().As<IRedisConnectionWrapper>()
.WithParameters(new[]
{
new NamedParameter("server", "127.0.0.1"),
new NamedParameter("pswd",""),
}).SingleInstance(); Container = builder.Build();

可以用下面的代码来测试一下上面这些方法。

public void ReadTest(long start, long end)
{
for (var i = start; i <= end; i++)
{
var exists = _redisDb.SetContains(RedisKey, i);
}
}

可以使用Windows版的Redis,直接运行redis-server.exe来启动服务。然后直接关闭redis-server程序来模拟服务端失败,或者直接禁用网卡来模拟网络失败。

可以看到Polly会进行重试并且捕获异常,也就说在ReadTest中感知不到异常。

搞定了同步方法,开始尝试动态代理异步方法。添加Redis异步接口的实现并注册:

public class RedisDatabaseAsyncWrapper:IDatabaseAsync
{
private IDatabase _redisDb; public RedisDatabaseAsyncWrapper(IRedisConnectionWrapper redisConnectionWrapper)
{
_redisDb = redisConnectionWrapper.Database();
} public virtual async Task<bool> SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
{
return await _redisDb.SetContainsAsync(key, value, flags);
} // 省略其它实现..
} //注册异步实现
builder.RegisterType<RedisDatabaseAsyncWrapper>().As<IDatabaseAsync>().EnableInterfaceInterceptors().InterceptedBy(typeof(RetryByPollyInterceptor)).SingleInstance(); //异步代理
private void InterceptAsync(IInvocation invocation)
{
var tsArr = new TimeSpan[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)
}; var policy = Policy
.Handle<TimeoutException>()
.Or<RedisConnectionException>()
.Or<Exception>()
.WaitAndRetry(tsArr); void OrignalInvoke()
{
try
{
invocation.Proceed();
}
catch (Exception e)
{
var geneType = invocation.Method.ReturnType.GenericTypeArguments[0];
var ret = geneType.IsValueType ? Activator.CreateInstance(geneType) : null;
invocation.ReturnValue = Task.FromResult(ret); Console.WriteLine(e);
}
} var pollyRet = policy.ExecuteAndCapture(OrignalInvoke,
new Dictionary<string, object>() { ["inv"] = invocation });
if (pollyRet.Outcome != OutcomeType.Successful)
{
Console.WriteLine(
$"Polly Ret Type:{pollyRet.Outcome} Exp:{pollyRet.ExceptionType} Msg:{pollyRet.FinalException?.Message}"); var invca = (IInvocation)pollyRet.Context["inv"]; var type = invca.Method.ReturnType;
if (type == typeof(void)) return;
if (type.IsGenericType)
{
var geneType = invca.Method.ReturnType.GenericTypeArguments[0];
var ret = geneType.IsValueType ? Activator.CreateInstance(geneType) : null;
invca.ReturnValue = Task.FromResult(ret);
}
else
{
invca.ReturnValue = Task.FromResult(0);
}
}
}

这里直接告诉各位我的尝试结果是无论如何都无法通过Polly来捕获异常。即上面代码中,OrignalInvoke方法中try...catch...抓不到异常,异常直接被扔给了外部方法。具体原因由于本人比较菜也比较懒没有仔细研究,大概可能就是用一个同步环境去调异步环境的方法没有特殊处理所以出的问题。有知道的园友评论中指点下。

如果是把invocation.Proceed()放在Task中,到是异常不会抛到外侧,但会因为被代理的方法取不到返回值而报空引用错误。原因大概应该是Castle.Core没有取到这个异步构造中的返回值。

经过一番尝试后放弃。在查找解决方法的过程中还发现一个名为Castle.Core.AsyncInterceptor的库,给Castle.Core添加动态代理异步函数的功能,但此扩展的文档实在过长,而且粗略看了下还不支持针对Autofac等IoC容器的扩展,直接放弃。

后来机缘巧合看到了园友Lemon大神的介绍其AspectCore库的文章。留言问了下对异步方法支持的情况,Lemon大神立刻给了回复,还附送了一些使用的特别提示。于是立马安装尝试。

首先是最重要的代理方法,AspectCore原生对异步方法提供支持,代码写起来很简单:

public class RetryByPollyAspectCoreInterceptor : AbstractInterceptorAttribute
{
public override async Task Invoke(AspectContext context, AspectDelegate next)
{
var tsArr = new TimeSpan[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)
}; var policy = Policy
.Handle<AspectInvocationException>(ex=>ex.InnerException?.GetType()==typeof(TimeoutException))
.Or<AspectInvocationException>(ex=>ex.InnerException?.GetType()==typeof(RedisConnectionException))
.WaitAndRetryAsync(tsArr); async Task OrignalInvoke()
{
await context.Invoke(next);
} var pollyRet = await policy.ExecuteAndCaptureAsync(OrignalInvoke,new Dictionary<string, object>() { ["ctx"] = context});
if (pollyRet.Outcome != OutcomeType.Successful)
{
Console.WriteLine($"Polly Ret Type:{pollyRet.Outcome} Exp:{pollyRet.ExceptionType} Msg:{pollyRet.FinalException?.Message}"); var ctx = (AspectContext)pollyRet.Context["ctx"];
var type = ctx.ProxyMethod.ReturnType;
if (type == typeof(void)) return;
if (type.IsGenericType)
{
var geneType = type.GenericTypeArguments[0];
dynamic ret = geneType.IsValueType ? Activator.CreateInstance(geneType) : null;
ctx.ReturnValue = Task.FromResult(ret);
}
else
{
var ret = type.IsValueType ? Activator.CreateInstance(type) : null;
ctx.ReturnValue = Task.FromResult(ret);
}
}
}
}

AspectCore也有Autofac的扩展,注册也是非常简单:

builder.RegisterDynamicProxy();


不过AspectCore还是需要给被代理的类添加Attribute:

[RetryByPollyAspectCoreInterceptor]
public class RedisDatabaseAsyncWrapper:IDatabaseAsync
{
...
}

希望大神可以扩展AspectCore的Autofac插件实现无需Attribute的代理设置。

2018/01/18补充

根据Lemon大神在评论中指点,AspectCore可以使用如下方式在Autofac注册中进行全局AOP注入:

builder.RegisterDynamicProxy(config =>
{
config.Interceptors.AddTyped<RetryByPollyAspectCoreInterceptor>(
Predicates.ForService("IDatabaseAsync"));
});

最后可以使用下面的代码测试这个异步的重试实现:

public async Task ReadTestAsync(long start, long end)
{
var total = end - start;
for (var i = 0; i <= total; i++)
{
var item = i + start;
var exists = await _redisDb.SetContainsAsync(RedisKey, item);
}
}

可以看到代理方法完美的处理了异常。

文末,在这个异步方法越来越多的新时代再次强烈推荐AspectCore

感谢各位大神提供了这么多好用的库。感谢各位园友阅读本文。

优雅的处理Redis访问超时的更多相关文章

  1. .net core 下使用StackExchange的Redis库访问超时解决

    原文:.net core 下使用StackExchange的Redis库访问超时解决 目录 问题:并发稍微多的情况下Redis偶尔返回超时 给出了参考网址? 结论 小备注 引用链接 问题:并发稍微多的 ...

  2. hystrix完成对redis访问的资源隔离

    相对来说,考虑的比较完善的一套方案,分为事前,事中,事后三个层次去思考怎么来应对缓存雪崩的场景 1.事前解决方案 发生缓存雪崩之前,事情之前,怎么去避免redis彻底挂掉 redis本身的高可用性,复 ...

  3. Redis实战-详细配置-优雅的使用Redis注解/RedisTemplate

    1. 简介 当我们对redis的基本知识有一定的了解后,我们再通过实战的角度学习一下在SpringBoot环境下,如何优雅的使用redis. 我们通过使用SpringBoot内置的Redis注解(文章 ...

  4. 如何优雅地用Redis实现分布式锁?

    转: 如何优雅地用Redis实现分布式锁?   BaiduSpring 01-2500:01 什么是分布式锁 在学习Java多线程编程的时候,锁是一个很重要也很基础的概念,锁可以看成是多线程情况下访问 ...

  5. redis访问安全加固

    目录 redis漏洞 入侵特征 安全隐患 redis安全规范 禁止root用户启动 限制redis文件目录访问权限 开启密码认证,设置复杂密码 禁用或重命名危险命令 设置允许监听地址,不要使用0.0. ...

  6. 使用kendynet构建异步redis访问服务

    使用kendynet构建异步redis访问服务 最近开始在kendynet上开发手游服务端,游戏类型是生存挑战类的,要存储的数据结构和类型都比较简单,于是选择了用redis做存储,数据类型使用stri ...

  7. Linux服务器可以ping,但是telnet端口超时,网站wget超时,访问超时的解决办法

    最近无法通过SSH连接Linux服务器,访问该服务器上的HTTP服务也出现异常.可以ping,但是telnet端口超时,网站wget超时,访问超时. 最后排查是内核配置问题 原来是 net.ipv4. ...

  8. 阿里云--安装nginx AND访问超时

    首先先安装PCRE pcre-devel 和Zlib,因为配置nginx的时候会需要这两个东西PCRE(Perl Compatible Regular Expressions) 是一个Perl库,包括 ...

  9. redis连接超时问题排查

    连接池无法获取到连接或获取连接超时redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource f ...

随机推荐

  1. nat的翻译类型(3)--端口地址转换

    目的:在1.1 1.2 1.3 三台内网的服务器访问外网的服务器(202.1.1.2)时,将内网ip转换为外网ip. 1.设置内网三台服务器的Ip ,网关,以及外网服务器的ip网关 分别为:192.1 ...

  2. IntelliJ IDEA(五) :Settings(中)

    上篇介绍了Settings中的Appearance & Behavior和Keymap,这篇继续,将介绍Editor,Plugins,Version Control. 一.Editor(编辑) ...

  3. 【ASP.NET MVC】MVC概述

    描述 本篇文章主要概述ASP.NET MVC,具体包括如下内容: 1.MVC模式概述 2.WebForm概述 3.WebForm与MVC区别 4.ASP.NET MVC发展历程 5.运用程序结构 6. ...

  4. 服务器固件测试--PCI设备的介绍(集成网卡和外插网卡)

    今天2017年9月26号,快三个月的时间,是该梳理一下,我来到这个岗位学到的东西. 网卡是什么 网卡分为俩大类 板载的集成网卡和外插的网卡.外插的网卡又分为很多种. 板载的集成网卡 外插的网卡分为 I ...

  5. JavaSe: String的编译期优化

    Java的编译期优化 因为工作的原因,经常会在没有源码的情况下,对一些产品的代码进行阅读.有时在解决Bug时,在运行环境下会直接去看class文件的字节码,来确定运行中版本是否正确的. 在看字节码时, ...

  6. spring-boot学习笔记之Conditional

    今天看了@Conditional,自己根据以下文章练了下,根据自己的理解操作的             转载出处:http://wiselyman.iteye.com/blog/2213054 17. ...

  7. 6.python内置函数

    1. abs() 获取绝对值 >>> abs(-10) 10 >>> a = -10 >>> a.__abs__() 10 2. all()   ...

  8. RabbitMQ阻塞读取时数据时,关闭channel引起的问题和解决方案

    项目场景: 最近在项目中使用了RabbitMq,其中有一个功能必须能随时切断RabbitMq的coumser.第一时间写出来的代码如下: 伪代码: while(flag){ QueueingConsu ...

  9. iOS App稳定性指标及监测

    一个App的稳定性,主要决定于整体的系统架构设计,同时也不可忽略编程的细节,正所谓"千里之堤,溃于蚁穴",一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃.尤其因为 ...

  10. JAVA Socket编程(二)之TCP通信

    基于TCP(面向连接)的socket编程,分为客户端和服务器端. 客户端的流程如下: (1)创建套接字(socket) (2)向服务器发出连接请求(connect) (3)和服务器端进行通信(send ...