nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

时间:2022-11-25 21:51:54

一.概述

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主从高可用缓存

软件版本

  • 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作为替代方案。

安装成功后任务栏会出现nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存小鲸鱼的图标,打开Hyper-V 管理器发现多出一个虚拟机,Docker就是在这个虚拟机中。

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

Settings配置

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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语法)配置文件。首先先看下我们的目录结构。

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

  • 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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

  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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

  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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

docker-compose up -d

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

好了,这样我们就完成了主从复制,和哨兵的配置了,是不是使用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" />

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

 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()方法

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

 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();
             }

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

 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的主容器,然后再恢复容器。

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

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

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

好了就先到这里吧。

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

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