背景

  有一套特定规格的应用(程序+数据库),当有业务需求时,就需要多部署应用,并且所有的应用都使用一个共同的后台来管理。应用新增后,如何通知后台更新连接串成了一个关键的问题。于是就产生了使用ZooKeeper管理数据库连接串的奇思异想。具体方案如下:

  

  1. 运维负责搭建数据库,并执行初始化脚本,然后把对应的数据库配置刷入ZooKeeper;

  2. 运维完成App(1...N)的部署,App(1...N)从ZooKeeper读取对应的数据库配置;

  3. 后台监听ZooKeeper,更新数据库配置到后台应用内存。

环境准备

  1. 安装Zookeeper

  docker pull zookeeper:3.4.13

  

  

  docker run --name zookeeper -d -p 2181:2181 zookeeper:3.4.13

  

  2. 安装Mysql

  docker pull mysql:5.7

  

  docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:5.7
  docker run --name mysql2 -e MYSQL_ROOT_PASSWORD=root -p 3307:3306 -d mysql:5.7
  docker run --name mysql3 -e MYSQL_ROOT_PASSWORD=root -p 3308:3306 -d mysql:5.7

  

  3. 初始化数据库

  1. CREATE DATABASE test;
  2. USE test;
  3. CREATE TABLE `table` (
  4. `id` int(11) NOT NULL,
  5. `name` varchar(50) NOT NULL,
  6. PRIMARY KEY (`id`)
  7. );

   

  分别在各个数据库插入测试数据

  mysql:

  1. USE test;
  2. INSERT INTO `table` (id, name) VALUES (1, 'A1');
  3. INSERT INTO `table` (id, name) VALUES (2, 'B1');
  4. INSERT INTO `table` (id, name) VALUES (3, 'C1');

  mysql2:

  1. USE test;
  2. INSERT INTO `table` (id, name) VALUES (1, 'A2');
  3. INSERT INTO `table` (id, name) VALUES (2, 'B2');
  4. INSERT INTO `table` (id, name) VALUES (3, 'C2');

  mysql3:

  1. USE test;
  2. INSERT INTO `table` (id, name) VALUES (1, 'A3');
  3. INSERT INTO `table` (id, name) VALUES (2, 'B3');
  4. INSERT INTO `table` (id, name) VALUES (3, 'C3');

  4. 基于数据库生成POCO

  Install-Package MySql.Data.EntityFrameworkCore -Version 8.0.13

  Scaffold-DbContext "server=127.0.0.1;port=3306;user=root;password=123456;database=test" MySql.Data.EntityFrameworkCore -OutputDir DataAccess -f

  

  

  5. 引用ZooKeeper相关组件

  Install-Package ZooKeeperNetEx -Version 3.4.12.1

  

核心代码

  1. ZookeeperOption:从appsettings中读取ZooKeeper相关配置

  1. public class ZookeeperOption
  2. {
  3. public ZookeeperOption(IConfiguration config)
  4. {
  5. if (config == null)
  6. {
  7. throw new ArgumentNullException(nameof(config));
  8. }
  9.  
  10. var section = config.GetSection("zookeeper");
  11. section.Bind(this);
  12. }
  13.  
  14. public string ConnectionString { get; set; }
  15.  
  16. public int Timeout { get; set; }
  17. }

  2. ZookeeperServiceCollectionExtensions:注册ZooKeeper服务

  1. public static class ZookeeperServiceCollectionExtensions
  2. {
  3. public static IServiceCollection AddZookeeper(this IServiceCollection services, IConfiguration config)
  4. {
  5. if (services == null)
  6. {
  7. throw new ArgumentNullException(nameof(services));
  8. }
  9.  
  10. if (config == null)
  11. {
  12. throw new ArgumentNullException(nameof(config));
  13. }
  14.  
  15. services.AddOptions();
  16.  
  17. var option = new ZookeeperOption(config);
  18.  
  19. var zookeeper = new org.apache.zookeeper.ZooKeeper(option.ConnectionString, option.Timeout * , new DefaultWatcher());
  20.  
  21. services.Add(ServiceDescriptor.Singleton(zookeeper));
  22. return services;
  23. }
  24. }
  25.  
  26. public class DefaultWatcher : Watcher
  27. {
  28. public override Task process(WatchedEvent @event)
  29. {
  30. return Task.CompletedTask;
  31. }
  32. }

  3. ZookeeperHandler:ZooKeeper初始化及目录变化处理类,并把数据库连接信息写入程序内存

  1. public interface IZookeeperHandler
  2. {
  3. Task InitAsync();
  4.  
  5. Task RefreshAsync();
  6. }
  7.  
  8. public class ZookeeperHandler: IZookeeperHandler
  9. {
  10. private readonly org.apache.zookeeper.ZooKeeper _zooKeeper;
  11. private readonly IMemoryCache _cache;
  12.  
  13. public ZookeeperHandler(org.apache.zookeeper.ZooKeeper zooKeeper, IMemoryCache cache)
  14. {
  15. _zooKeeper = zooKeeper;
  16. _cache = cache;
  17. }
  18.  
  19. public async Task InitAsync()
  20. {
  21. await RefreshAsync();
  22. }
  23.  
  24. public async Task RefreshAsync()
  25. {
  26. var connDic = new Dictionary<string, string>();
  27.  
  28. var isExisted = await _zooKeeper.existsAsync("/connections");
  29. if (isExisted == null)
  30. {
  31. await _zooKeeper.createAsync("/connections", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  32. }
  33.  
  34. var connResult = await _zooKeeper.getChildrenAsync("/connections", new ConnectionWatcher(this));
  35.  
  36. foreach (var conn in connResult.Children)
  37. {
  38. var connData = await _zooKeeper.getDataAsync($"/connections/{conn}/value");
  39. var connStr = Encoding.UTF8.GetString(connData.Data);
  40. connDic[conn] = connStr;
  41. }
  42.  
  43. _cache.Set("connections", connDic);
  44. }
  45. }

  4. ConnectionWatcher:监听者,内容变化时调用ZookeeperHandler的RefreshAsync()方法,其中,变化只通知一次,因此需要再次建立监听

  1. public class ConnectionWatcher : Watcher
  2. {
  3. private readonly IZookeeperHandler _zookeeperService;
  4.  
  5. public ConnectionWatcher(IZookeeperHandler zookeeperService)
  6. {
  7. _zookeeperService = zookeeperService;
  8. }
  9.  
  10. public override async Task process(WatchedEvent @event)
  11. {
  12. var type = @event.get_Type();
  13.  
  14. if (type != Event.EventType.None)
  15. {
  16. await _zookeeperService.RefreshAsync();
  17. }
  18. }
  19. }

  5. ZookeeperApplicationBuilderExtensions:初始化

  1. public static class ZookeeperApplicationBuilderExtensions
  2. {
  3. public static IApplicationBuilder UseZookeeper(this IApplicationBuilder app)
  4. {
  5. var service = app.ApplicationServices.GetRequiredService<IZookeeperHandler>();
  6. service.InitAsync().Wait();
  7. return app;
  8. }
  9. }

  6. ContextProvider:根据Id从内存中读取对应的数据库连接串,并提供DbContext实例

  1. public interface IContextProvider
  2. {
  3. TestContext GetContext(string id);
  4. }
  5.  
  6. public class ContextProvider : IContextProvider
  7. {
  8. private readonly IMemoryCache _cache;
  9.  
  10. public ContextProvider(IMemoryCache cache)
  11. {
  12. _cache = cache;
  13. }
  14.  
  15. public TestContext GetContext(string id)
  16. {
  17. var dic = _cache.Get<Dictionary<string, string>>("connections");
  18. var connectionStr = dic[id];
  19.  
  20. var optionsBuilder = new DbContextOptionsBuilder<TestContext>();
  21. optionsBuilder.UseMySQL(connectionStr);
  22. return new TestContext(optionsBuilder.Options);
  23. }
  24. }

效果演示

  1. 刚开始没有任何连接信息

  

  2. 添加一个连接信息

  

  

  3. 查询连接对应的数据

  

  4. 再添加两个连接信息

  

  

  

  

  

  5. 查看ZooKeeper的信息

  docker run -it --rm --link zookeeper:zookeeper zookeeper:3.4.13 zkCli.sh -server zookeeper

  ls /connections

  get /connections/1/value

  get /connections/2/value

  get /connections/3/value

  

补充说明

  因为把数据库连接信息写到了程序内存中,因此,如果当ZooKeeper出现了故障:

  1. 老的(正在运行)应用正在使用的数据库不会受到影响,但无法监听到数据库信息的变化;

  2. 新的应用无法启动。

  ZooKeeper恢复后:

  1. 老的(正在运行)应用会重连,重新监听到数据库信息的变化

  2. 新的应用可以成功启动。

源码地址

  https://github.com/ErikXu/zookeeper-connection-management

[奇思异想]使用Zookeeper管理数据库连接串的更多相关文章

  1. [奇思异想]使用RabbitMQ实现定时任务

    背景 工作中经常会有定时任务的需求,常见的做法可以使用Timer.Quartz.Hangfire等组件,这次想尝试下新的思路,使用RabbitMQ死信队列的机制来实现定时任务,同时帮助再次了解Rabb ...

  2. c# 扩展方法奇思妙用基础篇八:Distinct 扩展(转载)

    转载地址:http://www.cnblogs.com/ldp615/archive/2011/08/01/distinct-entension.html 刚看了篇文章 <Linq的Distin ...

  3. c# 扩展方法奇思妙用

    # 扩展方法出来已久,介绍扩展方法的文章也很多,但都是笼统的.本人最近一直在思考扩展方法的应用,也悟出了一些,准备将这最近一段时间对扩展方法的思考,写成一个系列文章.每个文章只介绍一个应用方面,篇幅不 ...

  4. c# 扩展方法奇思妙用基础篇八:Distinct 扩展

    刚看了篇文章 <Linq的Distinct太不给力了>,文中给出了一个解决办法,略显复杂. 试想如果能写成下面的样子,是不是更简单优雅 var p1 = products.Distinct ...

  5. 池化技术(二)HikariCP是如何管理数据库连接的?

    基于依赖程序的版本信息:HikariCP:3.3.1               驱动程序mysql-connector-java:8.0.17 上一篇:Druid是如何管理数据库连接的 零.类图和流 ...

  6. 池化技术(一)Druid是如何管理数据库连接的?

    基于依赖程序的版本信息:druid:1.1.16               驱动程序mysql-connector-java:8.0.17 下一篇:HikariCP是如何管理数据库连接的 零.类图& ...

  7. c# 扩展方法奇思妙用集锦

    本文转载:http://www.cnblogs.com/ldp615/archive/2009/08/07/1541404.html 其中本人觉得很经典的:c# 扩展方法奇思妙用基础篇五:Dictio ...

  8. Zookeeper管理多个HBase集群

    zookeeper是hbase集群的"协调器".由于zookeeper的轻量级特性,因此我们可以将多个hbase集群共用一个zookeeper集群,以节约大量的服务器.多个hbas ...

  9. Mysql系列九:使用zookeeper管理远程Mycat配置文件、Mycat监控、Mycat数据迁移(扩容)

    一.使用zookeeper管理远程Mycat配置文件 环境准备: 虚拟机192.168.152.130: zookeeper,具体参考前面文章 搭建dubbo+zookeeper+dubboadmin ...

随机推荐

  1. 大规模使用 Apache Kafka 的20个最佳实践

    必读 | 大规模使用 Apache Kafka 的20个最佳实践 配图来源:书籍<深入理解Kafka> Apache Kafka是一款流行的分布式数据流平台,它已经广泛地被诸如New Re ...

  2. PHP 消息队列 详解

    前言:之前做过的一些项目中有时候会接触到消息队列,但是对消息队列并没有一个很清楚的认知,本篇文章将会详细分析和归纳一些笔记,以供后续学习. 一.消息对列概念 从本质上说消息对列就是一个队列结构的中间件 ...

  3. 浅谈JS中逗号运算符的用法

    阅读本文的前提是,你能区分什么是表达式,什么是语句.还有明确运算符和运算数都是些啥东西.所谓的表达式就是一个JavaScript的"短语",JavaScript的解释器可以计算它, ...

  4. selenium自动化测试python

    一.环境部署 1.selenium安装 pip3 install selenium 1.安装浏览器驱动 WebDriver 需要通过浏览器驱动来与浏览器交互,以下列出几种常用的浏览器驱动下载地址: C ...

  5. 关于snmp octet string和普通string问题

    我是获取的Octet String用String输出,输出的是一连串的2个16进制数 空格.:然后想对输出结果操作,得到我想要的值. 解决方案:private static string exchan ...

  6. python放弃篇(Django/爬虫)

    第一篇:Django系列 第二篇:爬虫系列 待续……

  7. 光刻技术的原理和EUV光刻技术前景

    本文转载自微信公众号 半导体技术天地, 链接 https://mp.weixin.qq.com/s/EEBkSQ_Yc8RYFO18VpO8ow

  8. react native( rn) 中关于navigationOptions中headerRight 获取navigation的问题 rn

    使用以下代码获取navigation static navigationOptions = ({ navigation, navigationOptions }) => { const { pa ...

  9. 《.NET手札》

    第一记 .net是一个平台,即.NET Framework平台 c#是基于.net平台的开发语言 .net的两种交互模式: B/S : 即浏览器(Browser)/服务器模式(Server)     ...

  10. mysql的The user specified as a definer (”@’%') does not exist 的解决办法

    两种可能: 1.用户权限不够 赋给用户所有权限试试 mysql> grant all privileges on *.* to root@"%" identified by ...