一.概述

nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用。Redis支持主从复制,HA,集群。

一般来说,只有一台Redis是不可行的,原因如下:

  1. 单台Redis服务器会发生单点故障,并且单服务器需要处理所有的请求会导致压力较大。
  2. 单台Redis服务器内存容量有限,不易扩展。

第一个问题可以通过Redis主从模式实现单节点的高可用(HA)。

  • 从节点(slave)是主节点(master)副本,当主节点(master)宕机后,Redis 哨兵(Sentinel)会自动将从节点(slave)提升为主节点。
  • 由于一个master可以有多个slave,这样就可以实现读写分离,master负责写,slave负责读取。
  • slave同步master数据分为完整同步和部分重同步,首次启动或slave长时间离线后重启后会完整同步,当slave短时间离线会执行部分重同步,除非slave出现异常会执行完整同步。所以建议单台redis不要存储太大的数据,数据太大同步时间也会很长。

第二个问题可以通过Redis-cluster集群解决。(本篇不介绍)

本篇介绍的是第一个问题的解决方案,即主从模式及哨兵。

二.前期准备

测试环境

使用Docker部署Redis环境。由于本机操作系统windows 10 专业版,所以使用 docker for windows(下载)

  • windows 10 (部署 Web服务器)
  • docker for windows (部署 Redis)

架构

  • 1台 web服务器,部署nopCommerce
  • 1台 Master 主服务器
  • 1台 Slave 从服务器
  • 3台 Sentinel 哨兵服务器,用于检测master状态,负责主备切换。

架构图如下:

软件版本

  • nopCommerce 3.9
  • redis 4.0

三.Docker for Windows  搭建Docker环境

Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署。这里就不多介绍了,总之很方便。

由于大波浪操作系统是Win 10 所以使用Docker for Windows (下载)

Docker for Windows 安装需要满足以下条件

  • 64位Windows 10 Pro、Enterprise或者Education版本(Build 10586以上版本)
  • 系统启用Hyper-V。如果没有启用,Docker for Windows在安装过程中会自动启用Hyper-V(这个过程需要重启系统)
  • 如果不是使用的Windows 10,也没有关系,可以使用Docker Toolbox作为替代方案。

安装成功后任务栏会出现小鲸鱼的图标,打开Hyper-V 管理器发现多出一个虚拟机,Docker就是在这个虚拟机中。

右键点击小鲸鱼,Settings菜单用于配置Docker,Kitematic菜单用于打开Kitematic(一款可视化Docker交互容器)。

Settings配置

首先设置共享目录(Shared Drivers),为Docker容器与宿主机实现目录共享,Docker中叫做volume。

然后设置Docker Hub 镜像站,我用的是阿里云。如何配置参考我的另一篇博客

打开PowerShell输入docker version 出现下图信息恭喜你安装成功。

如果你安装了kitematic,打开后出现下图。

Docker的三个基本概念
  • 镜像(Image)

Docker 镜像(Image)就是一个只读的模板。
例如:一个镜像可以包含一个完整的 ubuntu 操作系统环境,里面仅安装了 Redis 或用户需要的其它应用程序。
镜像可以用来创建 Docker 容器。
Docker 提供了一个很简单的机制来创建镜像或者更新现有的镜像,用户甚至可以 直接从其他人那里下载一个已经做好的镜像来直接使用。

  • 容器 (Container)

Docker 利用容器(Container)来运行应用,比如 本篇Redis主从,哨兵都是部署在不同的容器中。
容器是从镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是 相互隔离的、保证安全的平台。
可以把容器看做是一个简易版的 linux 环境(包括root用户权限、进程空间、用户 空间和网络空间等)和运行在其中的应用程序。
*注:镜像是只读的,容器在启动的时候创建一层可写层作为最上层。

  • 仓库(Repository)

仓库(Repository)是集中存放镜像文件的场所。有时候会把仓库和仓库注册服务 器(Registry)混为一谈,并不严格区分。实际上,仓库注册服务器上往往存放着 多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。
仓库分为公开仓库(Public)和私有仓库(Private)两种形式。
最大的公开仓库是 Docker Hub,存放了数量庞大的镜像供用户下载。
国内的公开仓库包括 时速云 、网易云 等,可以提供大陆用户更稳定快速的访问。
当然,用户也可以在本地网络内创建一个私有仓库(参考本文“私有仓库”部分)。
当用户创建了自己的镜像之后就可以使用 push 命令将它上传到公有或者私有仓 库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来 就可以了。
*注:Docker 仓库的概念跟 Git 类似,注册服务器可以理解为 GitHub 这样的托管服 务。

四.Redis主从及哨兵配置

Redis slave 从服务器配置

只需要在从服务器的redis.conf文件中找到

# slaveof <masterip> <masterport>,   masterip是主服务器ip,masterport为主服务器端口。

试验中将slave的配置文件修改成 修改成  slaveof redis-master 6379

Redis sentinel 哨兵服务器配置

 #常规配置:
 port 26379
 daemonize yes
 logfile "/var/log/redis/sentinel.log"

 sentinel monitor mymaster redis-master 6379 2       #配置master名、ip、port、需要多少个sentinel才能判断[客观下线]
 sentinel down-after-milliseconds mymaster 30000     #配置sentinel向master发出ping,最大响应时间、超过则认为主观下线
 sentinel parallel-syncs mymaster  1                 #配置在进行故障转移时,运行多少个slave进行数据备份同步(越少速度越快)
 sentinel failover-timeout mymaster  180000          #配置当出现failover时下一个sentinel与上一个sentinel对[同一个master监测的时间间隔](最后设置为客观下线)

五.Docker 中部署 Redis

Docker可以通过run命令创建容器,可以通过Dockerfile文件创建自定义的image(镜像),也可以通过docker-compose通过模板快速部署多个容器。由于我们需要构建5个docker容器我们使用docker-compose方式。

使用docker-compose需要一个docker-compose.yml(基于YUML语法)配置文件。首先先看下我们的目录结构。

  • redis 为根目录
    • docker-compose.yml 部署配置文件
    • sentinel  文件夹用于放置 sentinel镜像相关文件
      • Dockerfile  sentinel镜像文件
      • sentinel-entrypoint.sh    sentinel容器启动时执行的命令
      • sentinel.conf    redis-sentinel的配置文件,用于配置哨兵服务。

dos2unix.exe  用于将windows 脚本 转换成 unix 下的脚本。主要转换sentinel-entrypoint.sh文件用的。

首先看下docker-compose.yml 文件

 master:
   image: redis:4
   ports:
      - 7010:6379
 slave:
   image: redis:4
   command: redis-server --slaveof redis-master 6379
   ports:
      - 7011:6379
   links:
     - master:redis-master
 sentinel:
   build: sentinel
   environment:
     - SENTINEL_QUORUM =2
     - SENTINEL_DOWN_AFTER=5000
     - SENTINEL_FAILOVER=5000
   links:
     - master:redis-master
     - slave

  1. 服务名称这里配置了 master,slave ,sentinel三个服务(后续 docker-compose scale 命令可以用到)。
  2. image  指定为镜像名称或镜像 ID,这里使用  redis 4.0版本
  3. ports  设置端口映射 7010:6379 代表外部7010端口映射到容器内部6379端口
  4. command 覆盖容器启动后默认执行的命令,这里是当slave 容器启动时关联主服务器 redis-master
  5. links  链接到其它服务中的容器。使用服务名称(同时作为别名)或服务名称:服务别名 (SERVICE:ALIAS) 格式都可以。这里关联master服务并且使用别名redis-master。
  6. build  指定 Dockerfile 所在文件夹的路径。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。这里指定当前目录下的sentinel文件夹
  7. environment  设置环境变量。你可以使用数组或字典两种格式。只给定名称的变量会自动获取它在 Compose 主机上的值,可以用来防止泄露不必要的数据。这里环境变量用于配置sentinel.conf的参数。

docker-compose.yml 文件定义了三个服务,master 为主服务,slave为从服务,sentinel为哨兵服务,都是基于redis 4.0镜像,

master:redis 主服务,对外公开的端口为7010 如果你在外部使用 Redis Dasktop这种工具通过7010端口就可以连接了。

slave:redis 从服务,对外公开端口为7011.同时通过links链接到 master容器实现容器间的通信。

sentinel:哨兵服务,使用Dockfile方式构建镜像,同时链接 master 和 slave容器。

接下来我们看下sentinel文件夹下的Dockefile文件,该文件用于构建哨兵镜像

 FROM redis:4

 MAINTAINER dabolang

 EXPOSE 26379
 ADD sentinel.conf /etc/redis/sentinel.conf
 RUN chown redis:redis /etc/redis/sentinel.conf
 ENV SENTINEL_QUORUM 2
 ENV SENTINEL_DOWN_AFTER 30000
 ENV SENTINEL_FAILOVER 180000

 COPY sentinel-entrypoint.sh /usr/local/bin/
 RUN chmod +x /usr/local/bin/sentinel-entrypoint.sh

 ENTRYPOINT ["sentinel-entrypoint.sh"]
 

DockerFile

  1. FROM 定义基于redis 4.0 镜像
  2. MAINTAINER  镜像创建者
  3. EXPOSE  容器内部服务开启的端口,哨兵服务默认端口是 26379.
  4. ADD  复制本地sentinel.conf 哨兵配置文件 拷贝到 容器的 /etc/redis/sentinel.conf
  5. RUN 容器中运行命令 chown redis:redis /etc/redis/sentinel.conf
  6. ENV  创建的时候给容器中加上个需要的环境变量。指定一个值,为后续的RUN指令服务。
  7. COPY 复制本地sentinel-entrypoint.sh文件到容器中/usr/local/bin/目录下
  8. RUN  为7中复制的脚本文件赋予执行权限。
  9. ENTRYPOINT  配置容器启动后执行的命令,并且不可被docker run 提供的参数覆盖,本例中启动后执行 7中复制的脚本。

sentinel.conf文件为哨兵文件。

 # Example sentinel.conf can be downloaded from http://download.redis.io/redis-stable/sentinel.conf
 # $SENTINEL_QUORUM 2   判断Sentinel失效的数量,超过failover才会执行
 # $SENTINEL_DOWN_AFTER  5000 指定了Sentinel认为Redis实例已经失效所需的毫秒数
 # $SENTINEL_FAILOVER  5000  如果在该时间(ms)内未能完成failover操作,则认为该failover失败

 #=======================================================================================
 # 监听端口号
 #=======================================================================================
 port 26379

 #=======================================================================================
 # Sentinel服务运行时使用的临时文件夹
 #=======================================================================================
 dir /tmp

 #=======================================================================================
 # Sentinel去监视一个名为mymaster的主redis实例,
 # 这个主实例的为Docker,redis-master容器(也可以是IP),端口号为6379,
 # 主实例判断为失效至少需要$SENTINEL_QUORUM个Sentinel进程的同意,只要同意Sentinel的数量不达标,自动failover就不会执行
 # sentinel monitor mymaster redis-master 6379 2
 #=======================================================================================
 sentinel monitor mymaster redis-master 6379 $SENTINEL_QUORUM

 #=======================================================================================
 # 指定了Sentinel认为Redis实例已经失效所需的毫秒数$SENTINEL_DOWN_AFTER,默认是30000毫秒。
 # 当实例超过该时间没有返回PING,或者直接返回错误,那么Sentinel将这个实例标记为主观下线。
 # 只有一个Sentinel进程将实例标记为主观下线并不一定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之后,实例才会被标记为客观下线,这时自动故障迁移才会执行
 # sentinel down-after-milliseconds mymaster 30000
 #=======================================================================================
 sentinel down-after-milliseconds mymaster $SENTINEL_DOWN_AFTER

 #=======================================================================================
 # 指定了在执行故障转移时,最多可以有多少个从Redis实例在同步新的主实例,
 # 在从Redis实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长
 #=======================================================================================
 sentinel parallel-syncs mymaster 1

 #=======================================================================================
 # 如果在该时间(ms)内未能完成failover操作,则认为该failover失败
 # sentinel failover-timeout mymaster 180000
 #=======================================================================================
 sentinel failover-timeout mymaster $SENTINEL_FAILOVER

 #指定sentinel检测到该监控的redis实例指向的实例异常时,调用的报警脚本。该配置项可选,但是很常用
 # Example:
 #
 # sentinel notification-script mymaster /var/redis/notify.sh

sentinel.conf

sentinel-entrypoint.sh 脚本通过配置的ENV环境变量替换sentinel.conf中的参数,最后通过exec 命令启动哨兵服务。

 #!/bin/sh

 sed -i "s/\$SENTINEL_QUORUM/$SENTINEL_QUORUM/g" /etc/redis/sentinel.conf
 sed -i "s/\$SENTINEL_DOWN_AFTER/$SENTINEL_DOWN_AFTER/g" /etc/redis/sentinel.conf
 sed -i "s/\$SENTINEL_FAILOVER/$SENTINEL_FAILOVER/g" /etc/redis/sentinel.conf

 exec docker-entrypoint.sh redis-server /etc/redis/sentinel.conf --sentinel

通过以上配置完整的docker-compose.yml 模板就制作完成了,别急还差最后一步。

PowerShell 中进入放置docker-compose.yml的目录redis,输入build命令 重建镜像。

docker-compose build

最后输入up命令生成容器。

docker-compose up -d

打开kitematic 或者 输入 docker ps –a 看到我们生成的容器了吧。

点击redis_master_1查看主服务状态,对外的接口也是7010.

我们通过Redis Manager 就可以链接了。

我们再看下我们的redis_slave_1从服务器。我们发现不仅对外暴露了7011端口,同时容器内部也关联了主服务。

接下来我们测试下主从是否成功。链接主服务器,输入 set master ok 命令插入一条记录。

从服务器 输入 get master 获取值为ok,主从复制成功。

主从配置成功了,我们发现只创建了一个redis_sentinel_1 哨兵容器,我们至少需要3台才能完成切换。

我们通过 docker-compose scale sentinel=3 命令创建 3个sentinel容器。

添加成功后我们发现3个哨兵服务器,并且哨兵容器也记录了其他的哨兵信息。

我们在从服务器中输入info Replication 命令查看到 role:slave 并且master主机ip为 172.17.0.2(docker内部ip)

现在模拟master主服务器宕机,选中redis_master_1点击STOP按钮,日志中我们看到 在08:53:15时刻关闭主服务器

点击哨兵服务器,我们发现08:53:20时发现master服务器宕机,间隔是5秒,为什么是5秒?因为哨兵服务器配置中我们设置的5秒钟,通过另外两台哨兵服务器投票超过了2票最终确认master服务器宕机。然后把slave主机上升为master主机,这里需要注意如果刚才宕机的服务器又正常启动了,这时候该服务器会变为slave服务器,并不会恢复原来的master身份。

我们在salve中输入info Replication命令,查看下该服务是不是上升为master服务了.

role:master,同时connected_saves:0 说明有0个从服务器。

接下来我们把刚才停掉的容器恢复,我们发现connected_saves:1说明宕机的主机恢复后会降为slave服务器。

好了,这样我们就完成了主从复制,和哨兵的配置了,是不是使用docker配置环境很简单。

如果部署中遇到问题,又修改了,记得先用docker-compose build 命令 再使用docker-compose up命令

六.nopCommerce 使用Redis作为缓存服务

Redis环境已经搭建好了,接下来我们修改nopCommerce中的代码来使用Redis.

  • nop使用StackExchange.Redis开源项目作为Redis 客户端
  • 使用RedLock为 Redis 分布式锁

首先修改Web.config文件 将 RedisCaching 节点 Enabled设置为True。

ConnectionString设置我们上边的master(localhost:7010)和slave(localhost:7011)两台服务器。

<RedisCaching Enabled="true" ConnectionString="localhost:7010,localhost:7011" />

在Nop.Core项目Caching下拷贝RedisCacheManager.cs 和 RedisConnectionWrapper.cs,

并重命名为RedisMSCacheManager.cs,RedisMSConnectionWrapper.cs。

RedisMSCacheManager类中修改RemoveByPattern 和 Clear 方法,添加  if (server.IsConnected == false || server.IsSlave == true) continue;代码判断服务器是否连接是否是从服务。

 using System;
 using System.Text;
 using Newtonsoft.Json;
 using Nop.Core.Configuration;
 using Nop.Core.Infrastructure;
 using StackExchange.Redis;
 using System.Linq;

 namespace Nop.Core.Caching
 {
     /// <summary>
     /// 命名空间:Nop.Core.Caching
     /// 名    称:RedisMSCacheManager
     /// 功    能:
     /// 详    细:
     /// 版    本:1.0.0.0
     /// 文件名称:RedisMSCacheManager.cs
     /// 创建时间:2017-08-29 03:15
     /// 修改时间:2017-08-30 05:28
     /// 作    者:大波浪
     /// 联系方式:http://www.cnblogs.com/yaoshangjin
     /// 说    明:
     /// </summary>
     public partial class RedisMSCacheManager : ICacheManager
     {
         #region Fields
         private readonly IRedisConnectionWrapper _connectionWrapper;
         private readonly IDatabase _db;
         private readonly ICacheManager _perRequestCacheManager;

         #endregion

         #region Ctor

         public RedisMSCacheManager(NopConfig config, IRedisConnectionWrapper connectionWrapper)
         {
             if (String.IsNullOrEmpty(config.RedisCachingConnectionString))
                 throw new Exception("Redis connection string is empty");

             // ConnectionMultiplexer.Connect should only be called once and shared between callers
             this._connectionWrapper = connectionWrapper;

             this._db = _connectionWrapper.GetDatabase();
             this._perRequestCacheManager = EngineContext.Current.Resolve<ICacheManager>();
         }

         #endregion

         #region Utilities

         protected virtual byte[] Serialize(object item)
         {
             var jsonString = JsonConvert.SerializeObject(item);
             return Encoding.UTF8.GetBytes(jsonString);
         }
         protected virtual T Deserialize<T>(byte[] serializedObject)
         {
             if (serializedObject == null)
                 return default(T);

             var jsonString = Encoding.UTF8.GetString(serializedObject);
             return JsonConvert.DeserializeObject<T>(jsonString);
         }

         #endregion

         #region Methods

         /// <summary>
         /// Gets or sets the value associated with the specified key.
         /// </summary>
         /// <typeparam name="T">Type</typeparam>
         /// <param name="key">The key of the value to get.</param>
         /// <returns>The value associated with the specified key.</returns>
         public virtual T Get<T>(string key)
         {
             //little performance workaround here:
             //we use "PerRequestCacheManager" to cache a loaded object in memory for the current HTTP request.
             //this way we won't connect to Redis server 500 times per HTTP request (e.g. each time to load a locale or setting)
             if (_perRequestCacheManager.IsSet(key))
                 return _perRequestCacheManager.Get<T>(key);

             var rValue = _db.StringGet(key);
             if (!rValue.HasValue)
                 return default(T);
             var result = Deserialize<T>(rValue);

             _perRequestCacheManager.Set(key, result, 0);
             return result;
         }

         /// <summary>
         /// Adds the specified key and object to the cache.
         /// </summary>
         /// <param name="key">key</param>
         /// <param name="data">Data</param>
         /// <param name="cacheTime">Cache time</param>
         public virtual void Set(string key, object data, int cacheTime)
         {
             if (data == null)
                 return;

             var entryBytes = Serialize(data);
             var expiresIn = TimeSpan.FromMinutes(cacheTime);
             _db.StringSet(key, entryBytes, expiresIn);
         }

         /// <summary>
         /// Gets a value indicating whether the value associated with the specified key is cached
         /// </summary>
         /// <param name="key">key</param>
         /// <returns>Result</returns>
         public virtual bool IsSet(string key)
         {
             //little performance workaround here:
             //we use "PerRequestCacheManager" to cache a loaded object in memory for the current HTTP request.
             //this way we won't connect to Redis server 500 times per HTTP request (e.g. each time to load a locale or setting)
             if (_perRequestCacheManager.IsSet(key))
                 return true;

             return _db.KeyExists(key);
         }

         /// <summary>
         /// Removes the value with the specified key from the cache
         /// </summary>
         /// <param name="key">/key</param>
         public virtual void Remove(string key)
         {
             _db.KeyDelete(key);
             _perRequestCacheManager.Remove(key);
         }

         /// <summary>
         /// Removes items by pattern
         /// </summary>
         /// <param name="pattern">pattern</param>
         public virtual void RemoveByPattern(string pattern)
         {
             foreach (var ep in _connectionWrapper.GetEndPoints())
             {
                 var server = _connectionWrapper.GetServer(ep);
                 if (server.IsConnected == false || server.IsSlave == true) continue;
                 var keys = server.Keys(database: _db.Database, pattern: "*" + pattern + "*");
                 foreach (var key in keys)
                     Remove(key);
             }
         }

         /// <summary>
         /// Clear all cache data
         /// </summary>
         public virtual void Clear()
         {
             foreach (var ep in _connectionWrapper.GetEndPoints())
             {
                 var server = _connectionWrapper.GetServer(ep);
                 if (server.IsConnected == false|| server.IsSlave==true) continue;
                 //we can use the code below (commented)
                 //but it requires administration permission - ",allowAdmin=true"
                 //server.FlushDatabase();

                 //that's why we simply interate through all elements now
                 var keys = server.Keys(database: _db.Database);
                 foreach (var key in keys)
                     Remove(key);
             }
         }

         /// <summary>
         /// Dispose
         /// </summary>
         public virtual void Dispose()
         {
             //if (_connectionWrapper != null)
             //    _connectionWrapper.Dispose();
         }

         #endregion

     }
 }
 

RedisMSCacheManager 完整代码

RedisMSConnectionWrapper中修改GetConnection()方法

修改PerformActionWithLock方法 我们将RedLock实现的分布式锁改成StackExchange.Redis的LockTake来实现。

 using System;
 using System.Linq;
 using System.Net;
 using Nop.Core.Configuration;
 using RedLock;
 using StackExchange.Redis;

 namespace Nop.Core.Caching
 {
     /// <summary>
     /// Redis connection wrapper implementation
     /// </summary>
     public class RedisConnectionWrapper : IRedisConnectionWrapper
     {
         #region Fields

         private readonly NopConfig _config;
         private readonly Lazy<string> _connectionString;

         private volatile ConnectionMultiplexer _connection;
         private volatile RedisLockFactory _redisLockFactory;
         private readonly object _lock = new object();

         #endregion

         #region Ctor

         public RedisConnectionWrapper(NopConfig config)
         {
             this._config = config;
             this._connectionString = new Lazy<string>(GetConnectionString);
             this._redisLockFactory = CreateRedisLockFactory();
         }

         #endregion

         #region Utilities

         /// <summary>
         /// Get connection string to Redis cache from configuration
         /// </summary>
         /// <returns></returns>
         protected string GetConnectionString()
         {
             return _config.RedisCachingConnectionString;
         }

         /// <summary>
         /// Get connection to Redis servers
         /// </summary>
         /// <returns></returns>
         protected ConnectionMultiplexer GetConnection()
         {
             if (_connection != null && _connection.IsConnected) return _connection;

             lock (_lock)
             {
                 if (_connection != null && _connection.IsConnected) return _connection;

                 if (_connection != null)
                 {
                     //Connection disconnected. Disposing connection...
                     _connection.Dispose();
                 }

                 //Creating new instance of Redis Connection
                 _connection = ConnectionMultiplexer.Connect(_connectionString.Value);
             }

             return _connection;
         }

         /// <summary>
         /// Create instance of RedisLockFactory
         /// </summary>
         /// <returns>RedisLockFactory</returns>
         protected RedisLockFactory CreateRedisLockFactory()
         {
             //get password and value whether to use ssl from connection string
             var password = string.Empty;
             var useSsl = false;
             foreach (var option in GetConnectionString().Split(',').Where(option => option.Contains('=')))
             {
                 switch (option.Substring(0, option.IndexOf('=')).Trim().ToLowerInvariant())
                 {
                     case "password":
                         password = option.Substring(option.IndexOf('=') + 1).Trim();
                         break;
                     case "ssl":
                         bool.TryParse(option.Substring(option.IndexOf('=') + 1).Trim(), out useSsl);
                         break;
                 }
             }

             //create RedisLockFactory for using Redlock distributed lock algorithm
             return new RedisLockFactory(GetEndPoints().Select(endPoint => new RedisLockEndPoint
             {
                 EndPoint = endPoint,
                 Password = password,
                 Ssl = useSsl
             }));
         }

         #endregion

         #region Methods

         /// <summary>
         /// Obtain an interactive connection to a database inside redis
         /// </summary>
         /// <param name="db">Database number; pass null to use the default value</param>
         /// <returns>Redis cache database</returns>
         public IDatabase GetDatabase(int? db = null)
         {
             return GetConnection().GetDatabase(db ?? -1); //_settings.DefaultDb);
         }

         /// <summary>
         /// Obtain a configuration API for an individual server
         /// </summary>
         /// <param name="endPoint">The network endpoint</param>
         /// <returns>Redis server</returns>
         public IServer GetServer(EndPoint endPoint)
         {
             return GetConnection().GetServer(endPoint);
         }

         /// <summary>
         /// Gets all endpoints defined on the server
         /// </summary>
         /// <returns>Array of endpoints</returns>
         public EndPoint[] GetEndPoints()
         {
             return GetConnection().GetEndPoints();
         }

         /// <summary>
         /// Delete all the keys of the database
         /// </summary>
         /// <param name="db">Database number; pass null to use the default value<</param>
         public void FlushDatabase(int? db = null)
         {
             var endPoints = GetEndPoints();

             foreach (var endPoint in endPoints)
             {
                 GetServer(endPoint).FlushDatabase(db ?? -1); //_settings.DefaultDb);
             }
         }

         /// <summary>
         /// Perform some action with Redis distributed lock
         /// </summary>
         /// <param name="resource">The thing we are locking on</param>
         /// <param name="expirationTime">The time after which the lock will automatically be expired by Redis</param>
         /// <param name="action">Action to be performed with locking</param>
         /// <returns>True if lock was acquired and action was performed; otherwise false</returns>
         public bool PerformActionWithLock(string resource, TimeSpan expirationTime, Action action)
         {
             //use RedLock library
             using (var redisLock = _redisLockFactory.Create(resource, expirationTime))
             {
                 //ensure that lock is acquired
                 if (!redisLock.IsAcquired)
                     return false;

                 //perform action
                 action();
                 return true;
             }
         }

         /// <summary>
         /// Release all resources associated with this object
         /// </summary>
         public void Dispose()
         {
             //dispose ConnectionMultiplexer
             if (_connection != null)
                 _connection.Dispose();

             //dispose RedisLockFactory
             if (_redisLockFactory != null)
                 _redisLockFactory.Dispose();
         }

         #endregion
     }
 }
 

RedisConnectionWrapper 完整代码

最后在Nop.Web.Framework项目DependencyRegistrar中修改依赖注入项。

if (config.RedisCachingEnabled)
             {
               //  builder.RegisterType<RedisConnectionWrapper>().As<IRedisConnectionWrapper>().SingleInstance();
                 builder.RegisterType<RedisMSConnectionWrapper>().As<IRedisConnectionWrapper>().SingleInstance();
                 builder.RegisterType<RedisMSCacheManager>().As<ICacheManager>().Named<ICacheManager>("nop_cache_static").InstancePerLifetimeScope();
             }

重新编译项目再启动就可以了。

为了方便测试在Nop.Core.Tests项目添加测试类RedisMSCacheManagerTests.cs,同时添加App.config配置文件

 using System;
 using System.Configuration;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Nop.Core.Caching;
 using Nop.Core.Configuration;
 using Nop.Tests;
 using NUnit.Framework;
 using StackExchange.Redis;

 namespace Nop.Core.Tests.Caching
 {
     [TestFixture]
     public class RedisMSCacheManagerTests
     {
         private NopConfig _nopConfig;
         private RedisMSCacheManager cacheManager;
         private RedisMSConnectionWrapper _redisMSConnectionWrapper;
         [SetUp]
         public void SetUp()
         {
             _nopConfig = ConfigurationManager.GetSection("NopConfig") as NopConfig;
             cacheManager = new RedisMSCacheManager(_nopConfig, new RedisMSConnectionWrapper(_nopConfig));
             _redisMSConnectionWrapper = new RedisMSConnectionWrapper(_nopConfig);
         }

         [Test]
         public void Set_Cache_Test()
         {
             int i = 0;
             while (i < 150)
             {
                 var key = DateTime.UtcNow.ToString("hh-mm-ss");
                 try
                 {
                     cacheManager.Set(key, DateTime.UtcNow.ToString(), 600000);
                     var ok = "true";
                     Console.WriteLine(key + ":" + ok);
                 }
                 catch (Exception)
                 {
                     Console.WriteLine("出错了:" + key);
                 }

                 Thread.Sleep(500);
                 i++;
             }
             var a = 2;
         }
         [Test]
         public void Can_set_and_get_object_from_cache()
         {
             cacheManager.Set("some_key_1", 3, int.MaxValue);

             cacheManager.Get<int>("some_key_1").ShouldEqual(3);
         }
         [Test]
         public void Can_validate_whetherobject_is_cached()
         {
             cacheManager.Set("some_key_1", 3, int.MaxValue);
             cacheManager.Set("some_key_2", 4, int.MaxValue);

             cacheManager.IsSet("some_key_1").ShouldEqual(true);
             cacheManager.IsSet("some_key_3").ShouldEqual(false);
         }

         [Test]
         public void Can_clear_cache()
         {
             cacheManager.Set("some_key_1", 3, int.MaxValue);

             cacheManager.Clear();

             cacheManager.IsSet("some_key_1").ShouldEqual(false);
         }
         [Test]
         public void TestLock()
         {

             RedisKey _lockKey = "lock_key";
             var  _lockDuration = TimeSpan.FromSeconds(30);
             var mainTask=new Task(()=>_redisMSConnectionWrapper.PerformActionWithLock(_lockKey, _lockDuration, () =>
             {
                 Console.WriteLine("1.执行锁内任务-开始");
                 Thread.Sleep(10000);
                 Console.WriteLine("1.执行锁内任务-结束");
             }));
             mainTask.Start();
             Thread.Sleep(1000);
             var subOk = true;
             while (subOk)
             {
                 _redisMSConnectionWrapper.PerformActionWithLock(_lockKey, _lockDuration, () =>
                 {
                     Console.WriteLine("2.执行锁内任务-开始");
                     Console.WriteLine("2.执行锁内任务-结束");
                     subOk = false;
                 });
                 Thread.Sleep(1000);
             }
             Console.WriteLine("结束");
         }

     }
 }
 

RedisMSCacheManagerTests 完整代码

 <?xml version="1.0" encoding="utf-8"?>
 <configuration>
   <configSections>
     <section name="NopConfig" type="Nop.Core.Configuration.NopConfig, Nop.Core" requirePermission="false" />
   </configSections>
   <NopConfig>
     <RedisCaching Enabled="true" ConnectionString="localhost:7010,localhost:7011" />
   </NopConfig>
 <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" /></startup>
   <runtime>
     <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
       <dependentAssembly>
         <assemblyIdentity name="Autofac" publicKeyToken="17863af14b0044da" culture="neutral" />
         <bindingRedirect oldVersion="0.0.0.0-4.4.0.0" newVersion="4.4.0.0" />
       </dependentAssembly>
       <dependentAssembly>
         <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
         <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
       </dependentAssembly>
       <dependentAssembly>
         <assemblyIdentity name="StackExchange.Redis.StrongName" publicKeyToken="c219ff1ca8c2ce46" culture="neutral" />
         <bindingRedirect oldVersion="0.0.0.0-1.2.1.0" newVersion="1.2.1.0" />
       </dependentAssembly>
     </assemblyBinding>
   </runtime>
 </configuration>
 

App.config 完整代码

运行Set_Cache_Test(),并在过程中关闭docker的主容器,然后再恢复容器。

下图为测试中结果,当关闭redis 主服务器时出现异常,然后下一秒就恢复了,证明主备切换正常。

好了就先到这里吧。

本文地址:http://www.cnblogs.com/yaoshangjin/p/7456362.html

本文为大波浪原创、转载请注明出处。

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存的更多相关文章

  1. nopCommerce 3.9 大波浪系列 之 引擎 NopEngine

    本章涉及到的内容如下 1.EngineContext初始化IEngine实例 2.Autofac依赖注入初始化 3.AutoMapper框架初始化 4.启动任务初始化 一.EngineContext初 ...

  2. nopCommerce 3.9 大波浪系列 之 网页加载Widgets插件原理

    一.插件简介 插件用于扩展nopCommerce的功能.nopCommerce有几种类型的插件如:支付.税率.配送方式.小部件等(接口如下图),更多插件可以访问nopCommerce官网. 我们看下后 ...

  3. nopCommerce 3.9 大波浪系列 之 使用部署在Docker中的Redis缓存主从服务

    一.概述 nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用.Redis支持主从复制,HA,集群. 一般来说,只有一台Redis是不可行的,原因如下: 单台Redis服务器会发 ...

  4. nopCommerce 3.9 大波浪系列 之 global.asax

    一.nop的global.asax文件 nop3.9基于ASP.NET MVC 5框架开发,而ASP.NET MVC中global.asax文件包含全局应用程序事件的事件处理程序,它响应应用程序级别和 ...

  5. nopCommerce 3.9 大波浪系列 之 IWebHelper

    接口:Nop.Core.IWebHelper 实现:Nop.Core.WebHelper 测试:Nop.Core.Tests.WebHelperTests 简介:Web辅助类 功能:获取客户端IP地址 ...

  6. nopCommerce 3.9 大波浪系列 之 汉化-Roxy Fileman

    官网:http://www.roxyfileman.com/ 中文包:zh.json 1.将zh.json包拷贝到Nop.Admin项目中"Content\Roxy_Fileman\lang ...

  7. nopCommerce 3.9 大波浪系列 之 路由扩展 [多语言Seo的实现]

    一.nop种的路由注册 在Global.asax,Application_Start()方法中会进行路由注册,代码如下. public static void RegisterRoutes(Route ...

  8. nopCommerce 3.9 大波浪系列 之 事件机制(生产者、消费者)

    一.nop事件机制简介 应用场景:客户支付成功后,需要发送短信.邮件告知客户订单支付成功(短信.邮件由不同模块实现) 实现方法: 1.定义支付成功OrderPaidEvent事件. 2.定义短信,邮箱 ...

  9. nopCommerce 3.9 大波浪系列 之 开发支持多店的插件

    一.基础介绍 nop支持多店及多语言,本篇结合NivoSlider插件介绍下如何开发支持多商城的小部件. 主要接口如下: ISettingService 接口:设置接口,可实现多店配置. (点击接口介 ...

随机推荐

  1. 浅谈游戏中BOSS设计的思路

    对于大多数游戏来说,BOSS在其设计上都有着不可替代的作用,也是玩家印象最为深刻的一部分.近期自己也有在做BOSS的设计工作,有一些心得想要分享一下: 1.明确BOSS的设计目的 在设计之初,我们一定 ...

  2. RxSwift 系列(一) -- Observables

    为什么使用RxSwift? 我们编写的代码绝大多数都涉及对外部事件的响应.当用户点击操作时,我们需要编写一个@IBAction事件来响应.我们需要观察通知,以检测键盘何时改变位置.当网络请求响应数据时 ...

  3. Python爬取百度贴吧数据

    本渣除了工作外,在生活上还是有些爱好,有些东西,一旦染上,就无法自拔,无法上岸,从此走上一条不归路.花鸟鱼虫便是我坚持了数十年的爱好. 本渣还是需要上班,才能支持我的业余爱好.上班时间还是尽量访问外网 ...

  4. MyBatis+mysql 简单分页

    注意:limit不能跟动态内容 <select id="fenYe" parameterType="int" resultType="com.x ...

  5. tar命令(转)

    把常用的tar解压命令总结下,当作备忘: tar -c: 建立压缩档案-x:解压-t:查看内容-r:向压缩归档文件末尾追加文件-u:更新原压缩包中的文件 这五个是独立的命令,压缩解压都要用到其中一个, ...

  6. JavaSE中线程与并行API框架学习笔记——线程为什么会不安全?

    前言:休整一个多月之后,终于开始投简历了.这段时间休息了一阵子,又病了几天,真正用来复习准备的时间其实并不多.说实话,心里不是非常有底气. 这可能是学生时代遗留的思维惯性--总想着做好万全准备才去做事 ...

  7. java迭代器浅析

    简介 迭代器是遍历容器的一种常用方法,它屏蔽了容器的实现细节,无需暴露数据结构内部,就可以对容器进行遍历,迭代器本身也是一种设计模式,迭代是一种特殊的遍历方式 Iterator 在java中,迭代器接 ...

  8. 前端数据存储方案集合(cookie localStorage等)以及详解 (二)

    前端数据存储方案集合(cookie localStorage等)以及详解 (二) 在之前的文章中已经介绍到了 前端存储方案中的 cookie . 但是 cookie 的存储上限是 4KB. 如果超过了 ...

  9. POJ 2386 Lake Counting (简单深搜)

    Description Due to recent rains, water has pooled in various places in Farmer John's field, which is ...

  10. Pycharm中的加载多个项目

    使用Pycharm,总会创建几个项目文件,有时候又不想全部一个一个的打开,所以这时候需要一个项目共存的方法,现在说一下怎么项目共存. 中英文对照 英文:首先打开setting界面: 中文首先打开设置界 ...