设为首页 收藏本站
查看: 1906|回复: 0

[经验分享] nopCommerce 3.9 大波浪系列 之 使用部署在Docker中的Redis缓存主从服务

[复制链接]

尚未签到

发表于 2017-12-5 17:21:09 | 显示全部楼层 |阅读模式
一.概述
  nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用。Redis支持主从复制,HA,集群。
  一般来说,只有一台Redis是不可行的,原因如下:


  • 单台Redis服务器会发生单点故障,并且单服务器需要处理所有的请求会导致压力较大。
  • 单台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状态,负责主备切换。
  架构图如下:
DSC0000.png


  软件版本



  • nopCommerce 3.9
  • redis 4.0

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


  • Docker 官网地址:https://www.docker.com/
  • Docker for Windows:https://store.docker.com/editions/community/docker-ce-desktop-windows
  由于大波浪操作系统是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作为替代方案。
  安装成功后任务栏会出现 DSC0001.png 小鲸鱼的图标,打开Hyper-V 管理器发现多出一个虚拟机,Docker就是在这个虚拟机中。
DSC0002.png

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

  Settings配置
DSC0004.png

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

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

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

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


  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 哨兵服务器配置



  1 #常规配置:
  2 port 26379
  3 daemonize yes
  4 logfile "/var/log/redis/sentinel.log"
  5
  6 sentinel monitor mymaster redis-master 6379 2       #配置master名、ip、port、需要多少个sentinel才能判断[客观下线]
  7 sentinel down-after-milliseconds mymaster 30000     #配置sentinel向master发出ping,最大响应时间、超过则认为主观下线
  8 sentinel parallel-syncs mymaster  1                 #配置在进行故障转移时,运行多少个slave进行数据备份同步(越少速度越快)
  9 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语法)配置文件。首先先看下我们的目录结构。
DSC0009.png



  • 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 文件



  1 master:
  2   image: redis:4
  3   ports:
  4      - 7010:6379
  5 slave:
  6   image: redis:4
  7   command: redis-server --slaveof redis-master 6379
  8   ports:
  9      - 7011:6379
10   links:
11     - master:redis-master
12 sentinel:
13   build: sentinel
14   environment:
15     - SENTINEL_QUORUM =2
16     - SENTINEL_DOWN_AFTER=5000
17     - SENTINEL_FAILOVER=5000
18   links:
19     - master:redis-master
20     - slave
DSC00010.png



  • 服务名称这里配置了 master,slave ,sentinel三个服务(后续 docker-compose scale 命令可以用到)。
  • image  指定为镜像名称或镜像 ID,这里使用  redis 4.0版本
  • ports  设置端口映射 7010:6379 代表外部7010端口映射到容器内部6379端口
  • command 覆盖容器启动后默认执行的命令,这里是当slave 容器启动时关联主服务器 redis-master
  • links  链接到其它服务中的容器。使用服务名称(同时作为别名)或服务名称:服务别名 (SERVICE:ALIAS) 格式都可以。这里关联master服务并且使用别名redis-master。
  • build  指定 Dockerfile 所在文件夹的路径。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。这里指定当前目录下的sentinel文件夹
  • 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文件,该文件用于构建哨兵镜像


DSC00011.gif DSC00012.gif


  1 FROM redis:4
  2
  3 MAINTAINER dabolang
  4
  5 EXPOSE 26379
  6 ADD sentinel.conf /etc/redis/sentinel.conf
  7 RUN chown redis:redis /etc/redis/sentinel.conf
  8 ENV SENTINEL_QUORUM 2
  9 ENV SENTINEL_DOWN_AFTER 30000
10 ENV SENTINEL_FAILOVER 180000
11
12 COPY sentinel-entrypoint.sh /usr/local/bin/
13 RUN chmod +x /usr/local/bin/sentinel-entrypoint.sh
14
15 ENTRYPOINT ["sentinel-entrypoint.sh"]
16
DockerFile
DSC00013.png



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





  1 # Example sentinel.conf can be downloaded from http://download.redis.io/redis-stable/sentinel.conf
  2 # $SENTINEL_QUORUM 2   判断Sentinel失效的数量,超过failover才会执行
  3 # $SENTINEL_DOWN_AFTER  5000 指定了Sentinel认为Redis实例已经失效所需的毫秒数
  4 # $SENTINEL_FAILOVER  5000  如果在该时间(ms)内未能完成failover操作,则认为该failover失败
  5
  6 #=======================================================================================
  7 # 监听端口号
  8 #=======================================================================================
  9 port 26379
10
11 #=======================================================================================
12 # Sentinel服务运行时使用的临时文件夹
13 #=======================================================================================
14 dir /tmp
15
16 #=======================================================================================
17 # Sentinel去监视一个名为mymaster的主redis实例,
18 # 这个主实例的为Docker,redis-master容器(也可以是IP),端口号为6379,
19 # 主实例判断为失效至少需要$SENTINEL_QUORUM个Sentinel进程的同意,只要同意Sentinel的数量不达标,自动failover就不会执行
20 # sentinel monitor mymaster redis-master 6379 2
21 #=======================================================================================
22 sentinel monitor mymaster redis-master 6379 $SENTINEL_QUORUM
23
24 #=======================================================================================
25 # 指定了Sentinel认为Redis实例已经失效所需的毫秒数$SENTINEL_DOWN_AFTER,默认是30000毫秒。
26 # 当实例超过该时间没有返回PING,或者直接返回错误,那么Sentinel将这个实例标记为主观下线。
27 # 只有一个Sentinel进程将实例标记为主观下线并不一定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之后,实例才会被标记为客观下线,这时自动故障迁移才会执行
28 # sentinel down-after-milliseconds mymaster 30000
29 #=======================================================================================
30 sentinel down-after-milliseconds mymaster $SENTINEL_DOWN_AFTER
31
32 #=======================================================================================
33 # 指定了在执行故障转移时,最多可以有多少个从Redis实例在同步新的主实例,
34 # 在从Redis实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长
35 #=======================================================================================
36 sentinel parallel-syncs mymaster 1
37
38 #=======================================================================================
39 # 如果在该时间(ms)内未能完成failover操作,则认为该failover失败
40 # sentinel failover-timeout mymaster 180000
41 #=======================================================================================
42 sentinel failover-timeout mymaster $SENTINEL_FAILOVER
43
44 #指定sentinel检测到该监控的redis实例指向的实例异常时,调用的报警脚本。该配置项可选,但是很常用
45 # Example:
46 #
47 # sentinel notification-script mymaster /var/redis/notify.sh
sentinel.conf  sentinel-entrypoint.sh 脚本通过配置的ENV环境变量替换sentinel.conf中的参数,最后通过exec 命令启动哨兵服务。



  1 #!/bin/sh
  2
  3 sed -i "s/\$SENTINEL_QUORUM/$SENTINEL_QUORUM/g" /etc/redis/sentinel.conf
  4 sed -i "s/\$SENTINEL_DOWN_AFTER/$SENTINEL_DOWN_AFTER/g" /etc/redis/sentinel.conf
  5 sed -i "s/\$SENTINEL_FAILOVER/$SENTINEL_FAILOVER/g" /etc/redis/sentinel.conf
  6
  7 exec docker-entrypoint.sh redis-server /etc/redis/sentinel.conf --sentinel
  通过以上配置完整的docker-compose.yml 模板就制作完成了,别急还差最后一步。
  PowerShell 中进入放置docker-compose.yml的目录redis,输入build命令 重建镜像。

  docker-compose build

DSC00014.png

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

  docker-compose up -d

DSC00015.png

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

DSC00017.png

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

DSC00019.png

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

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

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

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


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

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

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

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

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

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

DSC00029.png

  我们在salve中输入info Replication命令,查看下该服务是不是上升为master服务了.
  role:master,同时connected_saves:0 说明有0个从服务器。
DSC00030.png

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

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

DSC00032.png

  在Nop.Core项目Caching下拷贝RedisCacheManager.cs 和 RedisConnectionWrapper.cs,
  并重命名为RedisMSCacheManager.cs,RedisMSConnectionWrapper.cs。
DSC00033.png

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






  1 using System;
  2 using System.Text;
  3 using Newtonsoft.Json;
  4 using Nop.Core.Configuration;
  5 using Nop.Core.Infrastructure;
  6 using StackExchange.Redis;
  7 using System.Linq;
  8
  9 namespace Nop.Core.Caching
10 {
11     /// <summary>
12     /// 命名空间:Nop.Core.Caching
13     /// 名    称:RedisMSCacheManager
14     /// 功    能:
15     /// 详    细:
16     /// 版    本:1.0.0.0
17     /// 文件名称:RedisMSCacheManager.cs
18     /// 创建时间:2017-08-29 03:15
19     /// 修改时间:2017-08-30 05:28
20     /// 作    者:大波浪
21     /// 联系方式:http://www.cnblogs.com/yaoshangjin
22     /// 说    明:
23     /// </summary>
24     public partial class RedisMSCacheManager : ICacheManager
25     {
26         #region Fields
27         private readonly IRedisConnectionWrapper _connectionWrapper;
28         private readonly IDatabase _db;
29         private readonly ICacheManager _perRequestCacheManager;
30
31         #endregion
32
33         #region Ctor
34
35         public RedisMSCacheManager(NopConfig config, IRedisConnectionWrapper connectionWrapper)
36         {
37             if (String.IsNullOrEmpty(config.RedisCachingConnectionString))
38                 throw new Exception("Redis connection string is empty");
39
40             // ConnectionMultiplexer.Connect should only be called once and shared between callers
41             this._connectionWrapper = connectionWrapper;
42
43             this._db = _connectionWrapper.GetDatabase();
44             this._perRequestCacheManager = EngineContext.Current.Resolve<ICacheManager>();
45         }
46
47         #endregion
48
49         #region Utilities
50
51         protected virtual byte[] Serialize(object item)
52         {
53             var jsonString = JsonConvert.SerializeObject(item);
54             return Encoding.UTF8.GetBytes(jsonString);
55         }
56         protected virtual T Deserialize<T>(byte[] serializedObject)
57         {
58             if (serializedObject == null)
59                 return default(T);
60
61             var jsonString = Encoding.UTF8.GetString(serializedObject);
62             return JsonConvert.DeserializeObject<T>(jsonString);
63         }
64
65         #endregion
66
67         #region Methods
68
69         /// <summary>
70         /// Gets or sets the value associated with the specified key.
71         /// </summary>
72         /// <typeparam name="T">Type</typeparam>
73         /// <param name="key">The key of the value to get.</param>
74         /// <returns>The value associated with the specified key.</returns>
75         public virtual T Get<T>(string key)
76         {
77             //little performance workaround here:
78             //we use "PerRequestCacheManager" to cache a loaded object in memory for the current HTTP request.
79             //this way we won't connect to Redis server 500 times per HTTP request (e.g. each time to load a locale or setting)
80             if (_perRequestCacheManager.IsSet(key))
81                 return _perRequestCacheManager.Get<T>(key);
82
83             var rValue = _db.StringGet(key);
84             if (!rValue.HasValue)
85                 return default(T);
86             var result = Deserialize<T>(rValue);
87
88             _perRequestCacheManager.Set(key, result, 0);
89             return result;
90         }
91
92         /// <summary>
93         /// Adds the specified key and object to the cache.
94         /// </summary>
95         /// <param name="key">key</param>
96         /// <param name="data">Data</param>
97         /// <param name="cacheTime">Cache time</param>
98         public virtual void Set(string key, object data, int cacheTime)
99         {
100             if (data == null)
101                 return;
102
103             var entryBytes = Serialize(data);
104             var expiresIn = TimeSpan.FromMinutes(cacheTime);
105             _db.StringSet(key, entryBytes, expiresIn);
106         }
107
108         /// <summary>
109         /// Gets a value indicating whether the value associated with the specified key is cached
110         /// </summary>
111         /// <param name="key">key</param>
112         /// <returns>Result</returns>
113         public virtual bool IsSet(string key)
114         {
115             //little performance workaround here:
116             //we use "PerRequestCacheManager" to cache a loaded object in memory for the current HTTP request.
117             //this way we won't connect to Redis server 500 times per HTTP request (e.g. each time to load a locale or setting)
118             if (_perRequestCacheManager.IsSet(key))
119                 return true;
120
121             return _db.KeyExists(key);
122         }
123
124         /// <summary>
125         /// Removes the value with the specified key from the cache
126         /// </summary>
127         /// <param name="key">/key</param>
128         public virtual void Remove(string key)
129         {
130             _db.KeyDelete(key);
131             _perRequestCacheManager.Remove(key);
132         }
133
134         /// <summary>
135         /// Removes items by pattern
136         /// </summary>
137         /// <param name="pattern">pattern</param>
138         public virtual void RemoveByPattern(string pattern)
139         {
140             foreach (var ep in _connectionWrapper.GetEndPoints())
141             {
142                 var server = _connectionWrapper.GetServer(ep);
143                 if (server.IsConnected == false || server.IsSlave == true) continue;
144                 var keys = server.Keys(database: _db.Database, pattern: "*" + pattern + "*");
145                 foreach (var key in keys)
146                     Remove(key);
147             }
148         }
149
150         /// <summary>
151         /// Clear all cache data
152         /// </summary>
153         public virtual void Clear()
154         {
155             foreach (var ep in _connectionWrapper.GetEndPoints())
156             {
157                 var server = _connectionWrapper.GetServer(ep);
158                 if (server.IsConnected == false|| server.IsSlave==true) continue;
159                 //we can use the code below (commented)
160                 //but it requires administration permission - ",allowAdmin=true"
161                 //server.FlushDatabase();
162
163                 //that's why we simply interate through all elements now
164                 var keys = server.Keys(database: _db.Database);
165                 foreach (var key in keys)
166                     Remove(key);
167             }
168         }
169
170         /// <summary>
171         /// Dispose
172         /// </summary>
173         public virtual void Dispose()
174         {
175             //if (_connectionWrapper != null)
176             //    _connectionWrapper.Dispose();
177         }
178
179         #endregion
180
181     }
182 }
183
RedisMSCacheManager 完整代码  RedisMSConnectionWrapper中修改GetConnection()方法
DSC00035.png

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






  1 using System;
  2 using System.Linq;
  3 using System.Net;
  4 using Nop.Core.Configuration;
  5 using RedLock;
  6 using StackExchange.Redis;
  7
  8 namespace Nop.Core.Caching
  9 {
10     /// <summary>
11     /// 命名空间:Nop.Core.Caching
12     /// 名    称:RedisMSConnectionWrapper
13     /// 功    能:
14     /// 详    细:
15     /// 版    本:1.0.0.0
16     /// 文件名称:RedisMSConnectionWrapper.cs
17     /// 创建时间:2017-08-28 08:32
18     /// 修改时间:2017-08-29 04:39
19     /// 作    者:大波浪
20     /// 联系方式:http://www.cnblogs.com/yaoshangjin
21     /// 说    明:
22     /// </summary>
23     /// <summary>
24     /// Redis connection wrapper implementation
25     /// </summary>
26     public class RedisMSConnectionWrapper : IRedisConnectionWrapper
27     {
28         #region Fields
29
30         private readonly NopConfig _config;
31         private readonly Lazy<string> _connectionString;
32
33         private volatile ConnectionMultiplexer _connection;
34         private readonly object _lock = new object();
35
36         #endregion
37
38         #region Ctor
39
40         public RedisMSConnectionWrapper(NopConfig config)
41         {
42             this._config = config;
43             this._connectionString = new Lazy<string>(GetConnectionString);
44         }
45
46         #endregion
47
48         #region Utilities
49
50         /// <summary>
51         /// Get connection string to Redis cache from configuration
52         /// </summary>
53         /// <returns></returns>
54         protected string GetConnectionString()
55         {
56             return _config.RedisCachingConnectionString;
57         }
58
59         /// <summary>
60         /// Get connection to Redis servers
61         /// </summary>
62         /// <returns></returns>
63         protected ConnectionMultiplexer GetConnection()
64         {
65             if (_connection != null && _connection.IsConnected) return _connection;
66
67             lock (_lock)
68             {
69                 if (_connection != null && _connection.IsConnected) return _connection;
70
71                 if (_connection != null)
72                 {
73                     //Connection disconnected. Disposing connection...
74                     _connection.Dispose();
75                 }
76
77                 //Creating new instance of Redis Connection
78                 string configuration = _connectionString.Value;
79                 var config = ConfigurationOptions.Parse(configuration);
80                 _connection = ConnectionMultiplexer.Connect(config);
81                // _connection = ConnectionMultiplexer.Connect(_connectionString.Value);
82             }
83
84             return _connection;
85         }
86
87         ///// <summary>
88         ///// Create instance of RedisLockFactory
89         ///// </summary>
90         ///// <returns>RedisLockFactory</returns>
91         //protected RedisLockFactory CreateRedisLockFactory()
92         //{
93         //    //get password and value whether to use ssl from connection string
94         //    var password = string.Empty;
95         //    var useSsl = false;
96         //    foreach (var option in GetConnectionString().Split(',').Where(option => option.Contains('=')))
97         //    {
98         //        switch (option.Substring(0, option.IndexOf('=')).Trim().ToLowerInvariant())
99         //        {
100         //            case "password":
101         //                password = option.Substring(option.IndexOf('=') + 1).Trim();
102         //                break;
103         //            case "ssl":
104         //                bool.TryParse(option.Substring(option.IndexOf('=') + 1).Trim(), out useSsl);
105         //                break;
106         //        }
107         //    }
108
109         //    //create RedisLockFactory for using Redlock distributed lock algorithm
110         //    return new RedisLockFactory(GetEndPoints().Select(endPoint => new RedisLockEndPoint
111         //    {
112         //        EndPoint = endPoint,
113         //        Password = password,
114         //        Ssl = useSsl
115         //    }));
116         //}
117
118         #endregion
119
120         #region Methods
121
122         /// <summary>
123         /// Obtain an interactive connection to a database inside redis
124         /// </summary>
125         /// <param name="db">Database number; pass null to use the default value</param>
126         /// <returns>Redis cache database</returns>
127         public IDatabase GetDatabase(int? db = null)
128         {
129             return GetConnection().GetDatabase(db ?? -1); //_settings.DefaultDb);
130         }
131
132         /// <summary>
133         /// Obtain a configuration API for an individual server
134         /// </summary>
135         /// <param name="endPoint">The network endpoint</param>
136         /// <returns>Redis server</returns>
137         public IServer GetServer(EndPoint endPoint)
138         {
139             return GetConnection().GetServer(endPoint);
140         }
141
142         /// <summary>
143         /// Gets all endpoints defined on the server
144         /// </summary>
145         /// <returns>Array of endpoints</returns>
146         public EndPoint[] GetEndPoints()
147         {
148             return GetConnection().GetEndPoints();
149         }
150
151         /// <summary>
152         /// Delete all the keys of the database
153         /// </summary>
154         /// <param name="db">Database number; pass null to use the default value<</param>
155         public void FlushDatabase(int? db = null)
156         {
157             var endPoints = GetEndPoints();
158
159             foreach (var endPoint in endPoints)
160             {
161                 var server = GetServer(endPoint);
162                 if(server.IsConnected) server.FlushDatabase(db ?? -1); //_settings.DefaultDb);
163             }
164         }
165
166         /// <summary>
167         /// 分布式锁
168         /// </summary>
169         /// <param name="resource">The thing we are locking on</param>
170         /// <param name="expirationTime">The time after which the lock will automatically be expired by Redis</param>
171         /// <param name="action">Action to be performed with locking</param>
172         /// <returns>True if lock was acquired and action was performed; otherwise false</returns>
173         public bool PerformActionWithLock(string resource, TimeSpan expirationTime, Action action)
174         {
175             RedisValue lockToken = Guid.NewGuid().ToString(); //Environment.MachineName;
176             RedisKey locKey = resource;
177             var db = GetDatabase();
178             if (db.LockTake(locKey, lockToken, expirationTime))
179             {
180                 try
181                 {
182                     action();
183                 }
184                 finally
185                 {
186                     db.LockRelease(locKey, lockToken);
187                 }
188                 return true;
189             }
190             else
191             {
192                 return false;
193             }
194
195         }
196
197         /// <summary>
198         /// Release all resources associated with this object
199         /// </summary>
200         public void Dispose()
201         {
202             //dispose ConnectionMultiplexer
203             if (_connection != null)
204                 _connection.Dispose();
205         }
206
207         #endregion
208     }
209 }
RedisMSConnectionWrapper 完整代码  最后在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();
             }

DSC00037.png

  重新编译项目再启动就可以了。
  为了方便测试在Nop.Core.Tests项目添加测试类RedisMSCacheManagerTests.cs,同时添加App.config配置文件
DSC00038.png






  1 using System;
  2 using System.Configuration;
  3 using System.Net.Sockets;
  4 using System.Threading;
  5 using System.Threading.Tasks;
  6 using Nop.Core.Caching;
  7 using Nop.Core.Configuration;
  8 using Nop.Tests;
  9 using NUnit.Framework;
10 using StackExchange.Redis;
11
12 namespace Nop.Core.Tests.Caching
13 {
14     [TestFixture]
15     public class RedisMSCacheManagerTests
16     {
17         private NopConfig _nopConfig;
18         private RedisMSCacheManager cacheManager;
19         private RedisMSConnectionWrapper _redisMSConnectionWrapper;
20         [SetUp]
21         public void SetUp()
22         {
23             _nopConfig = ConfigurationManager.GetSection("NopConfig") as NopConfig;
24             cacheManager = new RedisMSCacheManager(_nopConfig, new RedisMSConnectionWrapper(_nopConfig));
25             _redisMSConnectionWrapper = new RedisMSConnectionWrapper(_nopConfig);
26         }
27
28         [Test]
29         public void Set_Cache_Test()
30         {
31             int i = 0;
32             while (i < 150)
33             {
34                 var key = DateTime.UtcNow.ToString("hh-mm-ss");
35                 try
36                 {
37                     cacheManager.Set(key, DateTime.UtcNow.ToString(), 600000);
38                     var ok = "true";
39                     Console.WriteLine(key + ":" + ok);
40                 }
41                 catch (Exception)
42                 {
43                     Console.WriteLine("出错了:" + key);
44                 }
45
46                 Thread.Sleep(500);
47                 i++;
48             }
49             var a = 2;
50         }
51         [Test]
52         public void Can_set_and_get_object_from_cache()
53         {
54             cacheManager.Set("some_key_1", 3, int.MaxValue);
55
56             cacheManager.Get<int>("some_key_1").ShouldEqual(3);
57         }
58         [Test]
59         public void Can_validate_whetherobject_is_cached()
60         {
61             cacheManager.Set("some_key_1", 3, int.MaxValue);
62             cacheManager.Set("some_key_2", 4, int.MaxValue);
63
64             cacheManager.IsSet("some_key_1").ShouldEqual(true);
65             cacheManager.IsSet("some_key_3").ShouldEqual(false);
66         }
67
68         [Test]
69         public void Can_clear_cache()
70         {
71             cacheManager.Set("some_key_1", 3, int.MaxValue);
72
73             cacheManager.Clear();
74
75             cacheManager.IsSet("some_key_1").ShouldEqual(false);
76         }
77         [Test]
78         public void TestLock()
79         {
80
81             RedisKey _lockKey = "lock_key";
82             var  _lockDuration = TimeSpan.FromSeconds(30);
83             var mainTask=new Task(()=>_redisMSConnectionWrapper.PerformActionWithLock(_lockKey, _lockDuration, () =>
84             {
85                 Console.WriteLine("1.执行锁内任务-开始");
86                 Thread.Sleep(10000);
87                 Console.WriteLine("1.执行锁内任务-结束");
88             }));
89             mainTask.Start();
90             Thread.Sleep(1000);
91             var subOk = true;
92             while (subOk)
93             {
94                 _redisMSConnectionWrapper.PerformActionWithLock(_lockKey, _lockDuration, () =>
95                 {
96                     Console.WriteLine("2.执行锁内任务-开始");
97                     Console.WriteLine("2.执行锁内任务-结束");
98                     subOk = false;
99                 });
100                 Thread.Sleep(1000);
101             }
102             Console.WriteLine("结束");
103         }
104
105     }
106 }
107
RedisMSCacheManagerTests 完整代码




  1 <?xml version="1.0" encoding="utf-8"?>
  2 <configuration>
  3   <configSections>
  4     <section name="NopConfig" type="Nop.Core.Configuration.NopConfig, Nop.Core" requirePermission="false" />
  5   </configSections>
  6   <NopConfig>
  7     <RedisCaching Enabled="true" ConnectionString="localhost:7010,localhost:7011" />
  8   </NopConfig>
  9 <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" /></startup>
10   <runtime>
11     <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
12       <dependentAssembly>
13         <assemblyIdentity name="Autofac" publicKeyToken="17863af14b0044da" culture="neutral" />
14         <bindingRedirect oldVersion="0.0.0.0-4.4.0.0" newVersion="4.4.0.0" />
15       </dependentAssembly>
16       <dependentAssembly>
17         <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
18         <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
19       </dependentAssembly>
20       <dependentAssembly>
21         <assemblyIdentity name="StackExchange.Redis.StrongName" publicKeyToken="c219ff1ca8c2ce46" culture="neutral" />
22         <bindingRedirect oldVersion="0.0.0.0-1.2.1.0" newVersion="1.2.1.0" />
23       </dependentAssembly>
24     </assemblyBinding>
25   </runtime>
26 </configuration>
27
App.config 完整代码  运行Set_Cache_Test(),并在过程中关闭docker的主容器,然后再恢复容器。
DSC00039.png

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

  好了就到这里吧,最后发下本文中所用到的代码。
  本篇相关代码:下载
  好了就先到这里吧。
  本文地址:http://www.cnblogs.com/yaoshangjin/p/7456378.html

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

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-420893-1-1.html 上篇帖子: Docker存储驱动之OverlayFS简介 下篇帖子: 运行第一个Docker容器-Docker for Web Developers(1)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表