【原創】強擼 .NET Redis Cluster 集群訪問組件

  Hello 大家好,我是TANZAME,我們又見面了。今天我們來聊聊怎麼手擼一個 Redis Cluster 集群客戶端,純手工有乾貨,您細品。

  隨着業務增長,線上環境的QPS暴增,自然而然將當前的單機 Redis 切換到群集模式。燃鵝,我們悲劇地發現,ServiceStack.Redis這個官方推薦的 .NET 客戶端並沒有支持集群模式。一通度娘翻牆無果后,決定自己強擼一個基於ServiceStack.Redis的Redis集群訪問組件。

  話不多說,先上運行效果圖:

 

  Redis-Cluster集群使用 hash slot 算法對每個key計算CRC16值,然後對16383取模,可以獲取key對應的 hash slot。Redis-Cluster中每個master都會持有部分 slot,在訪問key時根據計算出來的hash slot去找到具體的master節點,再由當前找到的節點去執行具體的 Redis 命令(具體可查閱官方說明文檔)。

  由於 ServiceStack.Redis已經實現了單個實例的Redis命令,因此我們可以將即將要實現的 Redis 集群客戶端當做一個代理,它只負責計算 key 落在哪一個具體節點(尋址)然後將Redis命令轉發給對應的節點執行即可。

  ServiceStack.Redis的RedisClient是非線程安全的,ServiceStack.Redis 使用緩存客戶端管理器(PooledRedisClientManager)來提高性能和併發能力,我們的Redis Cluster集群客戶端也應集成PooledRedisClientManager來獲取 RedisClient 實例。

  同時,Redis-Cluster集群支持在線動態擴容和slot遷移,我們的Redis集群客戶端也應具備自動智能發現新節點和自動刷新 slot 分佈的能力。

  總結起來,要實現一個Redis-Cluster客戶端,需要實現以下幾個要點:

  • 根據 key 計算 hash slot
  • 自動讀取群集上所有的節點信息
  • 為節點分配緩存客戶端管理器
  • 將 hash slot 路由到正確的節點
  • 自動發現新節點和自動刷新slot分佈

  如下面類圖所示,接下來我們詳細分析具體的代碼實現。

  

  一、CRC16  

  CRC即循環冗餘校驗碼,是信息系統中一種常見的檢錯碼。CRC校驗碼不同的機構有不同的標準,這裏Redis遵循的標準是CRC-16-CCITT標準,這也是被XMODEM協議使用的CRC標準,所以也常用XMODEM CRC代指,是比較經典的“基於字節查表法的CRC校驗碼生成算法”。 

 1 /// <summary>
 2 /// 根據 key 計算對應的哈希槽
 3 /// </summary>
 4 public static int GetSlot(string key)
 5 {
 6     key = CRC16.ExtractHashTag(key);
 7     // optimization with modulo operator with power of 2 equivalent to getCRC16(key) % 16384
 8     return GetCRC16(key) & (16384 - 1);
 9 }
10 
11 /// <summary>
12 /// 計算給定字節組的 crc16 檢驗碼
13 /// </summary>
14 public static int GetCRC16(byte[] bytes, int s, int e)
15 {
16     int crc = 0x0000;
17 
18     for (int i = s; i < e; i++)
19     {
20         crc = ((crc << 8) ^ LOOKUP_TABLE[((crc >> 8) ^ (bytes[i] & 0xFF)) & 0xFF]);
21     }
22     return crc & 0xFFFF;
23 }

 

  二、讀取集群節點

  從集群中的任意節點使用 CLUSTER NODES 命令可以讀取到集群中所有的節點信息,包括連接狀態,它們的標誌,屬性和分配的槽等等。CLUSTER NODES 以串行格式提供所有這些信息,輸出示例:

d99b65a25ef726c64c565901e345f98c496a1a47 127.0.0.1:7007 master - 0 1592288083308 8 connected
2d71879d6529d1edbfeed546443051986245c58e 127.0.0.1:7003 master - 0 1592288084311 11 connected 10923-16383
654cdc25a5fa11bd44b5b716cdf07d4ce176efcd 127.0.0.1:7005 slave 484e73948d8aacd8327bf90b89469b52bff464c5 0 1592288085313 10 connected
ed65d52dad7ef6854e0e261433b56a551e5e11cb 127.0.0.1:7004 slave 754d0ec7a7f5c7765f784a6a2c370ea38ea0c089 0 1592288081304 9 connected
754d0ec7a7f5c7765f784a6a2c370ea38ea0c089 127.0.0.1:7001 master - 0 1592288080300 9 connected 0-5460
484e73948d8aacd8327bf90b89469b52bff464c5 127.0.0.1:7002 master - 0 1592288082306 10 connected 5461-10922
2223bc6d099bd9838e5d2f1fbd9a758f64c554c4 127.0.0.1:7006 myself,slave 2d71879d6529d1edbfeed546443051986245c58e 0 0 6 connected

  每個字段的含義如下:

  1. id:節點 ID,一個40個字符的隨機字符串,當一個節點被創建時不會再發生變化(除非CLUSTER RESET HARD被使用)。

  2. ip:port:客戶端應該聯繫節點以運行查詢的節點地址。

  3. flags:逗號列表分隔的標誌:myselfmasterslavefail?failhandshakenoaddrnoflags。標誌在下一節詳細解釋。

  4. master:如果節點是從屬節點,並且主節點已知,則節點ID為主節點,否則為“ – ”字符。

  5. ping-sent:以毫秒為單位的當前激活的ping發送的unix時間,如果沒有掛起的ping,則為零。

  6. pong-recv:毫秒 unix 時間收到最後一個乒乓球。

  7. config-epoch:當前節點(或當前主節點,如果該節點是從節點)的配置時期(或版本)。每次發生故障切換時,都會創建一個新的,唯一的,單調遞增的配置時期。如果多個節點聲稱服務於相同的哈希槽,則具有較高配置時期的節點將獲勝。

  8. link-state:用於節點到節點集群總線的鏈路狀態。我們使用此鏈接與節點進行通信。可以是connecteddisconnected

  9. slot:散列槽號或範圍。從參數9開始,但總共可能有16384個條目(限制從未達到)。這是此節點提供的散列槽列表。如果條目僅僅是一個数字,則被解析為這樣。如果它是一個範圍,它是在形式start-end,並且意味着節點負責所有散列時隙從startend包括起始和結束值。

標誌的含義(字段編號3):

  • myself:您正在聯繫的節點。
  • master:節點是主人。
  • slave:節點是從屬的。
  • fail?:節點處於PFAIL狀態。對於正在聯繫的節點無法訪問,但仍然可以在邏輯上訪問(不處於FAIL狀態)。
  • fail:節點處於FAIL狀態。對於將PFAIL狀態提升為FAIL的多個節點而言,這是無法訪問的。
  • handshake:不受信任的節點,我們握手。
  • noaddr:此節點沒有已知的地址。
  • noflags:根本沒有標誌。
  1 // 讀取集群上的節點信息
  2 static IList<InternalClusterNode> ReadClusterNodes(IEnumerable<ClusterNode> source)
  3 {
  4     RedisClient c = null;
  5     StringReader reader = null;
  6     IList<InternalClusterNode> result = null;
  7 
  8     int index = 0;
  9     int rowCount = source.Count();
 10 
 11     foreach (var node in source)
 12     {
 13         try
 14         {
 15             // 從當前節點讀取REDIS集群節點信息
 16             index += 1;
 17             c = new RedisClient(node.Host, node.Port, node.Password);
 18             RedisData data = c.RawCommand("CLUSTER".ToUtf8Bytes(), "NODES".ToUtf8Bytes());
 19             string info = Encoding.UTF8.GetString(data.Data);
 20 
 21             // 將讀回的字符文本轉成強類型節點實體
 22             reader = new StringReader(info);
 23             string line = reader.ReadLine();
 24             while (line != null)
 25             {
 26                 if (result == null) result = new List<InternalClusterNode>();
 27                 InternalClusterNode n = InternalClusterNode.Parse(line);
 28                 n.Password = node.Password;
 29                 result.Add(n);
 30 
 31                 line = reader.ReadLine();
 32             }
 33 
 34             // 只要任意一個節點拿到集群信息,直接退出
 35             if (result != null && result.Count > 0) break;
 36         }
 37         catch (Exception ex)
 38         {
 39             // 出現異常,如果還沒到最後一個節點,則繼續使用下一下節點讀取集群信息
 40             // 否則拋出異常
 41             if (index < rowCount)
 42                 Thread.Sleep(100);
 43             else
 44                 throw new RedisClusterException(ex.Message, c != null ? c.GetHostString() : string.Empty, ex);
 45         }
 46         finally
 47         {
 48             if (reader != null) reader.Dispose();
 49             if (c != null) c.Dispose();
 50         }
 51     }
 52 
 53 
 54     if (result == null)
 55         result = new List<InternalClusterNode>(0);
 56     return result;
 57 }
 58 
 59 /// <summary>
 60 /// 從 cluster nodes 的每一行命令里讀取出集群節點的相關信息
 61 /// </summary>
 62 /// <param name="line">集群命令</param>
 63 /// <returns></returns>
 64 public static InternalClusterNode Parse(string line)
 65 {
 66     if (string.IsNullOrEmpty(line))
 67         throw new ArgumentException("line");
 68 
 69     InternalClusterNode node = new InternalClusterNode();
 70     node._nodeDescription = line;
 71     string[] segs = line.Split(' ');
 72 
 73     node.NodeId = segs[0];
 74     node.Host = segs[1].Split(':')[0];
 75     node.Port = int.Parse(segs[1].Split(':')[1]);
 76     node.MasterNodeId = segs[3] == "-" ? null : segs[3];
 77     node.PingSent = long.Parse(segs[4]);
 78     node.PongRecv = long.Parse(segs[5]);
 79     node.ConfigEpoch = int.Parse(segs[6]);
 80     node.LinkState = segs[7];
 81 
 82     string[] flags = segs[2].Split(',');
 83     node.IsMater = flags[0] == MYSELF ? flags[1] == MASTER : flags[0] == MASTER;
 84     node.IsSlave = !node.IsMater;
 85     int start = 0;
 86     if (flags[start] == MYSELF)
 87         start = 1;
 88     if (flags[start] == SLAVE || flags[start] == MASTER)
 89         start += 1;
 90     node.NodeFlag = string.Join(",", flags.Skip(start));
 91 
 92     if (segs.Length > 8)
 93     {
 94         string[] slots = segs[8].Split('-');
 95         node.Slot.Start = int.Parse(slots[0]);
 96         if (slots.Length > 1) node.Slot.End = int.Parse(slots[1]);
 97 
 98         for (int index = 9; index < segs.Length; index++)
 99         {
100             if (node.RestSlots == null)
101                 node.RestSlots = new List<HashSlot>();
102 
103             slots = segs[index].Split('-');
104 
105             int s1 = 0;
106             int s2 = 0;
107             bool b1 = int.TryParse(slots[0], out s1);
108             bool b2 = int.TryParse(slots[1], out s2);
109             if (!b1 || !b2)
110                 continue;
111             else
112                 node.RestSlots.Add(new HashSlot(s1, slots.Length > 1 ? new Nullable<int>(s2) : null));
113         }
114     }
115 
116     return node;
117 }

View Code

 

  三、為節點分配緩存客戶端管理器

  在單實例的Redis中,我們通過 PooledRedisClientManager 這個管理器來獲取RedisClient。借鑒這個思路,在Redis Cluster集群中,我們為每一個主節點實例化一個 PooledRedisClientManager,並且該主節點持有的 slot 都共享一個 PooledRedisClientManager 實例。以 slot 做為 key 將 slot 與 PooledRedisClientManager 一一映射並緩存起來。

 1 // 初始化集群管理
 2 void Initialize(IList<InternalClusterNode> clusterNodes = null)
 3 {
 4     // 從 redis 讀取集群信息
 5     IList<InternalClusterNode> nodes = clusterNodes == null ? RedisCluster.ReadClusterNodes(_source) : clusterNodes;
 6 
 7     // 生成主節點,每個主節點的 slot 對應一個REDIS客戶端緩衝池管理器
 8     IList<InternalClusterNode> masters = null;
 9     IDictionary<int, PooledRedisClientManager> managers = null;
10     foreach (var n in nodes)
11     {
12         // 節點無效或者
13         if (!(n.IsMater &&
14             !string.IsNullOrEmpty(n.Host) &&
15             string.IsNullOrEmpty(n.NodeFlag) &&
16             (string.IsNullOrEmpty(n.LinkState) || n.LinkState == InternalClusterNode.CONNECTED))) continue;
17 
18         n.SlaveNodes = nodes.Where(x => x.MasterNodeId == n.NodeId);
19         if (masters == null)
20             masters = new List<InternalClusterNode>();
21         masters.Add(n);
22 
23         // 用每一個主節點的哈希槽做鍵,導入REDIS客戶端緩衝池管理器
24         // 然後,方法表指針(又名類型對象指針)上場,佔據 4 個字節。 4 * 16384 / 1024 = 64KB
25         if (managers == null)
26             managers = new Dictionary<int, PooledRedisClientManager>();
27 
28         string[] writeHosts = new[] { n.HostString };
29         string[] readHosts = n.SlaveNodes.Where(n => false).Select(n => n.HostString).ToArray();
30         var pool = new PooledRedisClientManager(writeHosts, readHosts, _config);
31         managers.Add(n.Slot.Start, pool);
32         if (n.Slot.End != null)
33         {
34             // 這個範圍內的哈希槽都用同一個緩衝池
35             for (int s = n.Slot.Start + 1; s <= n.Slot.End.Value; s++)
36                 managers.Add(s, pool);
37         }
38         if (n.RestSlots != null)
39         {
40             foreach (var slot in n.RestSlots)
41             {
42                 managers.Add(slot.Start, pool);
43                 if (slot.End != null)
44                 {
45                     // 這個範圍內的哈希槽都用同一個緩衝池
46                     for (int s = slot.Start + 1; s <= slot.End.Value; s++)
47                         managers.Add(s, pool);
48                 }
49             }
50         }
51     }
52 
53     _masters = masters;
54     _redisClientManagers = managers;
55     _clusterNodes = nodes != null ? nodes : null;
56 
57     if (_masters == null) _masters = new List<InternalClusterNode>(0);
58     if (_clusterNodes == null) _clusterNodes = new List<InternalClusterNode>(0);
59     if (_redisClientManagers == null) _redisClientManagers = new Dictionary<int, PooledRedisClientManager>(0);
60 
61     if (_masters.Count > 0)
62         _source = _masters.Select(n => new ClusterNode(n.Host, n.Port, n.Password)).ToList();
63 }

View Code

 

  四、將 hash slot 路由到正確的節點

  在訪問一個 key 時,根據第三步緩存起來的 PooledRedisClientManager ,用 key 計算出來的 hash slot 值可以快速找出這個 key 對應的 PooledRedisClientManager 實例,調用 PooledRedisClientManager.GetClient() 即可將 hash slot 路由到正確的主節點。

 1 // 執行指定動作並返回值
 2 private T DoExecute<T>(string key, Func<RedisClient, T> action) => this.DoExecute(() => this.GetRedisClient(key), action);
 3 
 4 // 執行指定動作並返回值
 5 private T DoExecute<T>(Func<RedisClient> slot, Func<RedisClient, T> action, int tryTimes = 1)
 6 {
 7     RedisClient c = null;
 8     try
 9     {
10         c = slot();
11         return action(c);
12     }
13     catch (Exception ex)
14     {
15         // 此處省略 ...
16     }
17     finally
18     {
19         if (c != null)
20             c.Dispose();
21     }
22 }
23 
24 // 獲取指定key對應的主設備節點
25 private RedisClient GetRedisClient(string key)
26 {
27     if (string.IsNullOrEmpty(key))
28         throw new ArgumentNullException("key");
29 
30     int slot = CRC16.GetSlot(key);
31     if (!_redisClientManagers.ContainsKey(slot))
32         throw new SlotNotFoundException(string.Format("No reachable node in cluster for slot {{{0}}}", slot), slot, key);
33 
34     var pool = _redisClientManagers[slot];
35     return (RedisClient)pool.GetClient();
36 }

   

  五、自動發現新節點和自動刷新slot分佈

  在實際生產環境中,Redis 集群經常會有添加/刪除節點、遷移 slot 、主節點宕機從節點轉主節點等,針對這些情況,我們的 Redis Cluster 組件必須具備自動發現節點和刷新在 第三步  緩存起來的 slot 的能力。在這裏我的實現思路是當節點執行 Redis 命令時返回 RedisException 異常時就強制刷新集群節點信息並重新緩存 slot 與 節點之間的映射。

  1 // 執行指定動作並返回值
  2 private T DoExecute<T>(Func<RedisClient> slot, Func<RedisClient, T> action, int tryTimes = 1)
  3 {
  4     RedisClient c = null;
  5     try
  6     {
  7         c = slot();
  8         return action(c);
  9     }
 10     catch (Exception ex)
 11     {
 12         if (!(ex is RedisException) || tryTimes == 0) throw new RedisClusterException(ex.Message, c != null ? c.GetHostString() : string.Empty, ex);
 13         else
 14         {
 15             tryTimes -= 1;
 16             // 嘗試重新刷新集群信息
 17             bool isRefresh = DiscoveryNodes(_source, _config);
 18             if (isRefresh)
 19                 // 集群節點有更新過,重新執行
 20                 return this.DoExecute(slot, action, tryTimes);
 21             else
 22                 // 集群節點未更新過,直接拋出異常
 23                 throw new RedisClusterException(ex.Message, c != null ? c.GetHostString() : string.Empty, ex);
 24         }
 25     }
 26     finally
 27     {
 28         if (c != null)
 29             c.Dispose();
 30     }
 31 }
 32 
 33 // 重新刷新集群信息
 34 private bool DiscoveryNodes(IEnumerable<ClusterNode> source, RedisClientManagerConfig config)
 35 {
 36     bool lockTaken = false;
 37     try
 38     {
 39         // noop
 40         if (_isDiscoverying) { }
 41 
 42         Monitor.Enter(_objLock, ref lockTaken);
 43 
 44         _source = source;
 45         _config = config;
 46         _isDiscoverying = true;
 47 
 48         // 跟上次同步時間相隔 {MONITORINTERVAL} 秒鐘以上才需要同步
 49         if ((DateTime.Now - _lastDiscoveryTime).TotalMilliseconds >= MONITORINTERVAL)
 50         {
 51             bool isRefresh = false;
 52             IList<InternalClusterNode> newNodes = RedisCluster.ReadClusterNodes(_source);
 53             foreach (var node in newNodes)
 54             {
 55                 var n = _clusterNodes.FirstOrDefault(x => x.HostString == node.HostString);
 56                 isRefresh =
 57                     n == null ||                        // 新節點                                                                
 58                     n.Password != node.Password ||      // 密碼變了                                                                
 59                     n.IsMater != node.IsMater ||        // 主變從或者從變主                                                                
 60                     n.IsSlave != node.IsSlave ||        // 主變從或者從變主                                                                
 61                     n.NodeFlag != node.NodeFlag ||      // 節點標記位變了                                                                
 62                     n.LinkState != node.LinkState ||    // 節點狀態位變了                                                                
 63                     n.Slot.Start != node.Slot.Start ||  // 哈希槽變了                                                                
 64                     n.Slot.End != node.Slot.End ||      // 哈希槽變了
 65                     (n.RestSlots == null && node.RestSlots != null) ||
 66                     (n.RestSlots != null && node.RestSlots == null);
 67                 if (!isRefresh && n.RestSlots != null && node.RestSlots != null)
 68                 {
 69                     var slots1 = n.RestSlots.OrderBy(x => x.Start).ToList();
 70                     var slots2 = node.RestSlots.OrderBy(x => x.Start).ToList();
 71                     for (int index = 0; index < slots1.Count; index++)
 72                     {
 73                         isRefresh =
 74                             slots1[index].Start != slots2[index].Start ||   // 哈希槽變了                                                                
 75                             slots1[index].End != slots2[index].End;         // 哈希槽變了
 76                         if (isRefresh) break;
 77                     }
 78                 }
 79 
 80                 if (isRefresh) break;
 81             }
 82 
 83             if (isRefresh)
 84             {
 85                 // 重新初始化集群
 86                 this.Dispose();
 87                 this.Initialize(newNodes);
 88                 this._lastDiscoveryTime = DateTime.Now;
 89             }
 90         }
 91 
 92         // 最後刷新時間在 {MONITORINTERVAL} 內,表示是最新群集信息 newest
 93         return (DateTime.Now - _lastDiscoveryTime).TotalMilliseconds < MONITORINTERVAL;
 94     }
 95     finally
 96     {
 97         if (lockTaken)
 98         {
 99             _isDiscoverying = false;
100             Monitor.Exit(_objLock);
101         }
102     }
103 }

View Code

 

  六、配置訪問組件調用入口

  最後我們需要為組件提供訪問入口,我們用 RedisCluster 類實現 字符串、列表、哈希、集合、有序集合和Keys的基本操作,並且用 RedisClusterFactory 工廠類對外提供單例操作,這樣就可以像單實例 Redis 那樣調用 Redis Cluster 集群。調用示例:

var node = new ClusterNode("127.0.0.1", 7001);
var redisCluster = RedisClusterFactory.Configure(node, config);
string key = "B070x14668";
redisCluster.Set(key, key);
string value = redisCluster.Get<string>(key);
redisCluster.Del(key);
 1 /// <summary>
 2 /// REDIS 集群工廠
 3 /// </summary>
 4 public class RedisClusterFactory
 5 {
 6     static RedisClusterFactory _factory = new RedisClusterFactory();
 7     static RedisCluster _cluster = null;
 8 
 9     /// <summary>
10     /// Redis 集群
11     /// </summary>
12     public static RedisCluster Cluster
13     {
14         get
15         {
16             if (_cluster == null)
17                 throw new Exception("You should call RedisClusterFactory.Configure to config cluster first.");
18             else
19                 return _cluster;
20         }
21     }
22 
23     /// <summary>
24     /// 初始化 <see cref="RedisClusterFactory"/> 類的新實例
25     /// </summary>
26     private RedisClusterFactory()
27     {
28     }
29 
30     /// <summary>
31     /// 配置 REDIS 集群
32     /// <para>若群集中有指定 password 的節點,必須使用  IEnumerable&lt;ClusterNode&gt; 重載列舉出這些節點</para>
33     /// </summary>
34     /// <param name="node">集群節點</param>
35     /// <returns></returns>
36     public static RedisCluster Configure(ClusterNode node)
37     {
38         return RedisClusterFactory.Configure(node, null);
39     }
40 
41     /// <summary>
42     /// 配置 REDIS 集群
43     /// <para>若群集中有指定 password 的節點,必須使用  IEnumerable&lt;ClusterNode&gt; 重載列舉出這些節點</para>
44     /// </summary>
45     /// <param name="node">集群節點</param>
46     /// <param name="config"><see cref="RedisClientManagerConfig"/> 客戶端緩衝池配置</param>
47     /// <returns></returns>
48     public static RedisCluster Configure(ClusterNode node, RedisClientManagerConfig config)
49     {
50         return RedisClusterFactory.Configure(new List<ClusterNode> { node }, config);
51     }
52 
53     /// <summary>
54     /// 配置 REDIS 集群
55     /// </summary>
56     /// <param name="nodes">集群節點</param>
57     /// <param name="config"><see cref="RedisClientManagerConfig"/> 客戶端緩衝池配置</param>
58     /// <returns></returns>
59     public static RedisCluster Configure(IEnumerable<ClusterNode> nodes, RedisClientManagerConfig config)
60     {
61         if (nodes == null)
62             throw new ArgumentNullException("nodes");
63 
64         if (nodes == null || nodes.Count() == 0)
65             throw new ArgumentException("There is no nodes to configure cluster.");
66 
67         if (_cluster == null)
68         {
69             lock (_factory)
70             {
71                 if (_cluster == null)
72                 {
73                     RedisCluster c = new RedisCluster(nodes, config);
74                     _cluster = c;
75                 }
76             }
77         }
78 
79         return _cluster;
80     }
81 }

View Code

 

  總結

  今天我們詳細介紹了如何從0手寫一個Redis Cluster集群客戶端訪問組件,相信對同樣在尋找類似解決方案的同學們會有一定的啟發,喜歡的同學請點個 star。在沒有相同案例可以參考的情況下筆者通過查閱官方說明文檔和借鑒 Java 的 JedisCluster 的實現思路,雖說磕磕碰碰但最終也初步完成這個組件並投入使用,必須給自己加一個雞腿!!在此我有一個小小的疑問,.NET 的同學們在用 Redis 集群時,你們是用什麼組件耍的,為何網上的相關介紹和現成組件幾乎都沒有?歡迎討論。

  GitHub 代碼託管:https://github.com/TANZAME/ServiceStack.Redis.Cluster

  技術交流 QQ 群:816425449

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

聚甘新

荷蘭開放首條塑膠再製自行車道 約含50萬個瓶蓋

摘錄自2018年09月18日科技新報報導

荷蘭人以愛好騎單車聞名,該國約 1,700 萬人,卻擁有超過 2,200 萬輛腳踏車,光是阿姆斯特丹就鋪設了近 800 公里的自行車道,東北部城鎮茲沃勒(Zwolle)日前又正式開放一條全由回收塑膠再製生成的自行車道。

該塑膠車道概念來自道路工程公司 KWS 的 Anne Koudstaal 和 Simon Jorritsma,道路全長 30 公尺,採用預製模塊,重量輕、易安裝,下方設計了排水系統讓管道、電纜通過,可讓水快速流通,遇到暴雨時還能充當臨時儲水槽、避免淹水。

據估計,這條自行車道包含了約 218,000 個塑膠杯,或 50 萬個塑膠瓶蓋,使用年限可比傳統道路長 2~3 倍(有待觀察),路面應該也不會出現裂縫或坑洞。

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

印度雨季後氣候乾燥 德里登革熱疫情飆升

摘錄自2018年09月18日中央社報導

「印度斯坦時報」(Hindustan Times)18日引述南德里市政機構(SDMC)彙整新德里、東德里和北德里等另3個市政機構的統計顯示,德里地區今年迄今的登革熱病例達243例。

在15日之前的一個星期,德里地區就通報了106起登革熱病例,高達全年迄今243例的近一半。這段時間是德里近2個月連續降雨以來,首個未降雨的星期。而雨季後出現的乾燥氣候,專家認為是登革熱病例突然飆升的主因。醫護人員和專家都擔心,最近登革熱病例的上升,現在只是開端。

為防止登革熱疫情傳布,南德里市政機構最近已對社區大規模噴灑殺蟲劑。北德里市政機構也表示,正派檢查員調查和處罰未清理積水者。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

聚甘新

氣候模擬:廣設風光發電 有助撒哈拉沙漠綠化

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

造假醜聞、禁令、民眾信心跌 德國柴油車榮景恐回不去了

環境資訊中心記者 陳文姿報導

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新

微塑膠汙染新途徑 透過孑孓進入食物鏈

摘錄自2018年09月19日中央社報導

英國瑞丁大學(University of Reading)研究人員19日表示,蚊子幼蟲孑孓會吞食塑膠製品破裂後形成的微塑膠,被吞食的微粒中,許多會隨著孑孓的成長,轉移到成蚊體內,微塑膠可能透過蚊子和其他飛行昆蟲,從空中進入人類生態系。

同時,這也代表若有任何生物吃下這些會飛的蚊子,也會跟著吃進微小塑膠粒,而眾所周知,會捕食這類昆蟲的動物包括數種鳥類、蝙蝠和蜘蛛,這些動物又成為其他動物獵食的目標,進而在食物鏈內傳遞。

研究報告主要作者、瑞丁大學生物科學家賈拉漢(Amanda Callaghan)接受媒體訪問時談到:「這項發現的重要性是此現象可能相當普遍。我們只挑選蚊子作為樣本進行觀察,不過還有許多昆蟲活在水裡,生命週期也和孑孓一樣,會吞食水裡的東西,然後變成成蟲。」

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聚甘新

車商化敵為友挑戰Tesla,共同發展電動車充電網

市場上能見度高的汽車廠皆已跨入電動車領域,電動車市場顯得更加蓬勃,競爭也更加激烈,但也為車商創造了前所未見的合作組合。BMW、Diamler、Ford、Volkswagen旗下的Audi與Porsche宣布將在歐洲共同發展電動車充電網。

歐洲是全球最早開始發展電動車的地區之一,荷蘭、挪威等國都是電動車高普及國。雖然歐洲因人口與市場發展較早等因素,未來幾年的成長率預期將落後於中國、美國等地,但仍然是電動車產業的一大標的市場。

BBC報導,國際車商BMW、Diamler、Ford和Volkswagen將聯手在歐洲發展高效能的電動車充電網絡,預計將在全歐設置400處充電站。這些充電站將沿著主要道路設置,並為電動車提供超高速充電服務。這套充電網絡的基礎是一組結合式的標準充電系統科技,車主最高可享有350kW的充電功率,是目前市場上充電樁效能最高的設備。

不過,Tesla電動車因系統不相容,將無法使用這些充電站。

這些合作車商表示,他們希望發展一套裝置快速、可快速擴充充電站數量的系統,讓電動車車主們能享受遠程駕駛,不受電池續航力所限制。而這項合作將在2017年正式起跑。

將帶來車商與車主雙贏

電動車充電APP軟體Zap-Map的執行者Ben Lane對BBC表示,這是一個「超讚的消息,這正是產業需要的。」他指出,隨著電動車搭載的電池變大,充電時間也會越長;沿路都有快速充電系統對車主來說是個利多。

BBC則舉加油站的廣泛設置為例,說明這四間車商宣布將聯手打造綿密的電動車充電網的行動,是非常合理的;這不僅能提升電動車車主的使用方便性,也能說服更多人購買電動車。

雖然目前電動車充電時間仍不如直接加油來得快,但Tesla的超級快充站已經可在40分鐘內將Model S的電池充到八分飽,更有計畫將所需時間降到5~10分鐘。若BMW等四間車商能透過通力合作的方式發展更具效率的充電設備,電動車就會隨著這個計畫變得更具競爭力。

(照片來源:維基共享資源)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

聚甘新

Honda與微軟技術合作,推出AI電動車

日本汽車大廠本田(Honda)近日宣布,即將在下個月美國消費性電子展(CES 2017)中展出的最新電動概念車「NeuV」,將搭載軟銀(SoftBank)旗下雲端AI 技術研發公司cocoro SB 株式會社所打造的「情感引擎」,可望改變人與車之間冰冷的相處形態,增添更多情感交流與互動。

對汽車產業發展來說,人工智慧(AI)、機器人科技與大數據分析等,已逐漸成為改善行動體驗所不可或缺的重要技術。本田在今年7 月宣布,其子公司本田技術研究所(Honda R&D Co., Ltd.)將與軟銀合作,針對cocoro SB 所研發的情感引擎(emotion engine)共同研究有關應用。本田也在同年9 月於東京赤坂設置本田創新研究室「Honda R&D Innovation Lab Tokyo」,進一步研發AI 技術並強化其開放式創新發展。

提到cocoro SB 就不得不提人形機器人Pepper,因為Pepper 的情感引擎正是採用cocoro SB 的AI 雲端運算技術,在搭配攝影設備及多種感測器之下,能夠感知人類情緒與外在環境,從而建立自我情感並做出反應。

本田NeuV 概念車也採類似概念,透過系統收集而來的訊息判斷駕駛人的情緒,並學習做出適當反應加以交流對話,目標成為駕駛人的行動好夥伴,改變原本人與機器間的關係,同時也將為人們生活創造全新價值。至於NeuV 設計概念與技術細節,預計在下個月的CES 展中會有進一步說明。

(本文由《》授權提供。照片來源:Honda)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

聚甘新

樂視電動車CES亮相,宣戰特斯拉、法拉利與賓利

2017年首個國際大型電子展覽CES即將於1月3日在美國賭城開鑼。由中國樂視控股所投資成立的電動車新創公司「法拉第未來」(Faraday Future)宣布將在展會上正式推出第一款樂視電動車。

樂視控股創辦人兼董事長賈躍亭在12月11日於北京舉辦的「2016中國企業領袖年會」上揭露樂視電動車的首支概念影片,並表示法拉第未來的第一款電動車很快就會在CES 2017與世人見面。

賈躍亭強調,樂視電動車將是世界上最先進的汽車產品,結合了互聯網、AI(人工智慧)技術;加上樂視本身的IT、電視、手機產品,樂視電動車將成為智慧終端機。他進一步指出,樂視的超級汽車已經在中國浙江末干山取得設廠所需土地,未來將建設中國首個互聯網生態超級汽車工廠,且全程開放使用者參與。

外媒指出,樂視所公開的影片顯然把特斯拉(Tesla)、法拉利(Ferrari)以及賓利(Bentley)等超跑與電動車龍頭視為假想敵;但無論影片或者賈躍亭的演說,都未透露關於這款即將面世的電動車的其他細節,包括具體的名稱、價格區間、充電所需時間等等問題。這些問題都需等待CES 2017才能獲得答案。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

聚甘新

搶攻電動車商機,和大擬現增20億加快大埔美廠擴產

為了搶攻Tesla帶來的電動車商機,和大工業(1536)董事會昨(14)日通過提前啟動嘉義大埔美二期廠建廠計畫,將打造特斯拉專廠,預定將在2018年8月全面投產;同時和大也通過將辦理現金增資20億元,以因應擴產資金需求。

(Photo Credit:和大工業)

為加速大埔美二期廠建設,和大董事會通過斥資4.27億元,向同集團的高鋒工業(4510)購買緊鄰和大大埔美一期廠的土地廠房,並將斥資12億元,採購4條工業4.0的智能化產線設備。待新產能開出後,公司預期大埔美廠未來將貢獻和大半數產值,也有助於提前達成年營收百億元目標。

而為了因應產能擴張的資金需求,和大董事會也決議將辦理現金增資,預計發行2,000萬股新股,每股面額10元,暫定每股100元溢價發行,總計將籌募20億元資金。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新