很长一段时间以来,一直在项目中使用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. 【打CF,学算法——三星级】Codeforces Round #313 (Div. 2) C. Gerald&#39;s Hexagon

    [CF简单介绍] 提交链接:http://codeforces.com/contest/560/problem/C 题面: C. Gerald's Hexagon time limit per tes ...

  2. 聊聊属性方法property的用法

    写之前随便百度了一下博客,又看到廖雪峰的博客了.果然置顶的能力很强. 我想说其实property的用法并不是主要用来做类型检查.反而更多应该是用于简化操作的目的. 写之前想聊一个古老的话题.年初的时候 ...

  3. 让你用sublime写出最完美的python代码--windows环境

    至少很长一段时间内,我个人用的一直是pycharm,也感觉挺好用的,也没啥大毛病 但是pycharm确实有点笨重,啥功能都有,但是有很多可能这辈子我也不会用到,并且pycharm打开的速度确实不敢恭维 ...

  4. 《程序设计实践》【PDF】下载

    <程序设计实践>[PDF]下载链接: https://u253469.ctfile.com/fs/253469-231196319 内容简介 本书从排错.测试.性能.可移植性.设计.界面. ...

  5. 添加组groupadd,修改组groupmod,删除组groupdel,将用户加入删除组gpasswd

    groupadd -g GID :指定组id groupmod -g GID :修改组id -n 新组名 :修改组名 groupmod -n newname oldname groupdel grou ...

  6. iOS知识点、面试题 之三

    最近面试,发现这些题 还不错,与大家分享一下,分三文给大家: 当然Xcode新版本区别,以及iOS新特性 Xcode8 和iOS 10 在之前文章有发过,感兴趣的可以查阅: http://www.cn ...

  7. Java 哲学家进餐

    某次操作系统实验存档.V 这个哲学家除了吃就知道睡.( ╯□╰ ) 哲学家.java: package operating.entity.philosophyeating; import operat ...

  8. php-删除非空目录

    function deldir($path){ if(!is_dir($path)){ return false; } $dh = opendir($path); while(($file = rea ...

  9. Redis分布式集群搭建

    Redis集群架构图 上图蓝色为redis集群的节点. 节点之间通过ping命令来测试连接是否正常,节点之间没有主区分,连接到任何一个节点进行操作时,都可能会转发到其他节点. 1.Redis的容错机制 ...

  10. 用vue实现简单分页

    在这个demo中,我用vue对一个json文件中的数据进行了简单的分页,没用用到交互,一下是我的实现过程. 基础逻辑 1.将json文件引入app.vue,并作为data返回 data(){ var ...