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客户端,需要实现以下几个要点:

  如下面类图所示,接下来我们详细分析具体的代码实现。

  

  一、CRC16  

  CRC即循环冗余校验码,是信息系统中一种常见的检错码。CRC校验码不同的机构有不同的标准,这里Redis遵循的标准是CRC-16-CCITT标准,这也是被XMODEM协议使用的CRC标准,所以也常用XMODEM CRC代指,是比较经典的“基于字节查表法的CRC校验码生成算法”。

 /// <summary>
/// 根据 key 计算对应的哈希槽
/// </summary>
public static int GetSlot(string key)
{
key = CRC16.ExtractHashTag(key);
// optimization with modulo operator with power of 2 equivalent to getCRC16(key) % 16384
return GetCRC16(key) & ( - );
} /// <summary>
/// 计算给定字节组的 crc16 检验码
/// </summary>
public static int GetCRC16(byte[] bytes, int s, int e)
{
int crc = 0x0000; for (int i = s; i < e; i++)
{
crc = ((crc << ) ^ LOOKUP_TABLE[((crc >> ) ^ (bytes[i] & 0xFF)) & 0xFF]);
}
return crc & 0xFFFF;
}

  二、读取集群节点

  从集群中的任意节点使用 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:根本没有标志。

 // 读取集群上的节点信息
static IList<InternalClusterNode> ReadClusterNodes(IEnumerable<ClusterNode> source)
{
RedisClient c = null;
StringReader reader = null;
IList<InternalClusterNode> result = null; int index = ;
int rowCount = source.Count(); foreach (var node in source)
{
try
{
// 从当前节点读取REDIS集群节点信息
index += ;
c = new RedisClient(node.Host, node.Port, node.Password);
RedisData data = c.RawCommand("CLUSTER".ToUtf8Bytes(), "NODES".ToUtf8Bytes());
string info = Encoding.UTF8.GetString(data.Data); // 将读回的字符文本转成强类型节点实体
reader = new StringReader(info);
string line = reader.ReadLine();
while (line != null)
{
if (result == null) result = new List<InternalClusterNode>();
InternalClusterNode n = InternalClusterNode.Parse(line);
n.Password = node.Password;
result.Add(n); line = reader.ReadLine();
} // 只要任意一个节点拿到集群信息,直接退出
if (result != null && result.Count > ) break;
}
catch (Exception ex)
{
// 出现异常,如果还没到最后一个节点,则继续使用下一下节点读取集群信息
// 否则抛出异常
if (index < rowCount)
Thread.Sleep();
else
throw new RedisClusterException(ex.Message, c != null ? c.GetHostString() : string.Empty, ex);
}
finally
{
if (reader != null) reader.Dispose();
if (c != null) c.Dispose();
}
} if (result == null)
result = new List<InternalClusterNode>();
return result;
} /// <summary>
/// 从 cluster nodes 的每一行命令里读取出集群节点的相关信息
/// </summary>
/// <param name="line">集群命令</param>
/// <returns></returns>
public static InternalClusterNode Parse(string line)
{
if (string.IsNullOrEmpty(line))
throw new ArgumentException("line"); InternalClusterNode node = new InternalClusterNode();
node._nodeDescription = line;
string[] segs = line.Split(' '); node.NodeId = segs[];
node.Host = segs[].Split(':')[];
node.Port = int.Parse(segs[].Split(':')[]);
node.MasterNodeId = segs[] == "-" ? null : segs[];
node.PingSent = long.Parse(segs[]);
node.PongRecv = long.Parse(segs[]);
node.ConfigEpoch = int.Parse(segs[]);
node.LinkState = segs[]; string[] flags = segs[].Split(',');
node.IsMater = flags[] == MYSELF ? flags[] == MASTER : flags[] == MASTER;
node.IsSlave = !node.IsMater;
int start = ;
if (flags[start] == MYSELF)
start = ;
if (flags[start] == SLAVE || flags[start] == MASTER)
start += ;
node.NodeFlag = string.Join(",", flags.Skip(start)); if (segs.Length > )
{
string[] slots = segs[].Split('-');
node.Slot.Start = int.Parse(slots[]);
if (slots.Length > ) node.Slot.End = int.Parse(slots[]); for (int index = ; index < segs.Length; index++)
{
if (node.RestSlots == null)
node.RestSlots = new List<HashSlot>(); slots = segs[index].Split('-'); int s1 = ;
int s2 = ;
bool b1 = int.TryParse(slots[], out s1);
bool b2 = int.TryParse(slots[], out s2);
if (!b1 || !b2)
continue;
else
node.RestSlots.Add(new HashSlot(s1, slots.Length > ? new Nullable<int>(s2) : null));
}
} return node;
}

  三、为节点分配缓存客户端管理器

  在单实例的Redis中,我们通过 PooledRedisClientManager 这个管理器来获取RedisClient。借鉴这个思路,在Redis Cluster集群中,我们为每一个主节点实例化一个 PooledRedisClientManager,并且该主节点持有的 slot 都共享一个 PooledRedisClientManager 实例。以 slot 做为 key 将 slot 与 PooledRedisClientManager 一一映射并缓存起来。

 // 初始化集群管理
void Initialize(IList<InternalClusterNode> clusterNodes = null)
{
// 从 redis 读取集群信息
IList<InternalClusterNode> nodes = clusterNodes == null ? RedisCluster.ReadClusterNodes(_source) : clusterNodes; // 生成主节点,每个主节点的 slot 对应一个REDIS客户端缓冲池管理器
IList<InternalClusterNode> masters = null;
IDictionary<int, PooledRedisClientManager> managers = null;
foreach (var n in nodes)
{
// 节点无效或者
if (!(n.IsMater &&
!string.IsNullOrEmpty(n.Host) &&
string.IsNullOrEmpty(n.NodeFlag) &&
(string.IsNullOrEmpty(n.LinkState) || n.LinkState == InternalClusterNode.CONNECTED))) continue; n.SlaveNodes = nodes.Where(x => x.MasterNodeId == n.NodeId);
if (masters == null)
masters = new List<InternalClusterNode>();
masters.Add(n); // 用每一个主节点的哈希槽做键,导入REDIS客户端缓冲池管理器
// 然后,方法表指针(又名类型对象指针)上场,占据 4 个字节。 4 * 16384 / 1024 = 64KB
if (managers == null)
managers = new Dictionary<int, PooledRedisClientManager>(); string[] writeHosts = new[] { n.HostString };
string[] readHosts = n.SlaveNodes.Where(n => false).Select(n => n.HostString).ToArray();
var pool = new PooledRedisClientManager(writeHosts, readHosts, _config);
managers.Add(n.Slot.Start, pool);
if (n.Slot.End != null)
{
// 这个范围内的哈希槽都用同一个缓冲池
for (int s = n.Slot.Start + ; s <= n.Slot.End.Value; s++)
managers.Add(s, pool);
}
if (n.RestSlots != null)
{
foreach (var slot in n.RestSlots)
{
managers.Add(slot.Start, pool);
if (slot.End != null)
{
// 这个范围内的哈希槽都用同一个缓冲池
for (int s = slot.Start + ; s <= slot.End.Value; s++)
managers.Add(s, pool);
}
}
}
} _masters = masters;
_redisClientManagers = managers;
_clusterNodes = nodes != null ? nodes : null; if (_masters == null) _masters = new List<InternalClusterNode>();
if (_clusterNodes == null) _clusterNodes = new List<InternalClusterNode>();
if (_redisClientManagers == null) _redisClientManagers = new Dictionary<int, PooledRedisClientManager>(); if (_masters.Count > )
_source = _masters.Select(n => new ClusterNode(n.Host, n.Port, n.Password)).ToList();
}

  四、将 hash slot 路由到正确的节点

  在访问一个 key 时,根据第三步缓存起来的 PooledRedisClientManager ,用 key 计算出来的 hash slot 值可以快速找出这个 key 对应的 PooledRedisClientManager 实例,调用 PooledRedisClientManager.GetClient() 即可将 hash slot 路由到正确的主节点。

 // 执行指定动作并返回值
private T DoExecute<T>(string key, Func<RedisClient, T> action) => this.DoExecute(() => this.GetRedisClient(key), action); // 执行指定动作并返回值
private T DoExecute<T>(Func<RedisClient> slot, Func<RedisClient, T> action, int tryTimes = )
{
RedisClient c = null;
try
{
c = slot();
return action(c);
}
catch (Exception ex)
{
// 此处省略 ...
}
finally
{
if (c != null)
c.Dispose();
}
} // 获取指定key对应的主设备节点
private RedisClient GetRedisClient(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key"); int slot = CRC16.GetSlot(key);
if (!_redisClientManagers.ContainsKey(slot))
throw new SlotNotFoundException(string.Format("No reachable node in cluster for slot {{{0}}}", slot), slot, key); var pool = _redisClientManagers[slot];
return (RedisClient)pool.GetClient();
}

  

  五、自动发现新节点和自动刷新slot分布

  在实际生产环境中,Redis 集群经常会有添加/删除节点、迁移 slot 、主节点宕机从节点转主节点等,针对这些情况,我们的 Redis Cluster 组件必须具备自动发现节点和刷新在 第三步 缓存起来的 slot 的能力。在这里我的实现思路是当节点执行 Redis 命令时返回 RedisException 异常时就强制刷新集群节点信息并重新缓存 slot 与 节点之间的映射。

 // 执行指定动作并返回值
private T DoExecute<T>(Func<RedisClient> slot, Func<RedisClient, T> action, int tryTimes = )
{
RedisClient c = null;
try
{
c = slot();
return action(c);
}
catch (Exception ex)
{
if (!(ex is RedisException) || tryTimes == ) throw new RedisClusterException(ex.Message, c != null ? c.GetHostString() : string.Empty, ex);
else
{
tryTimes -= ;
// 尝试重新刷新集群信息
bool isRefresh = DiscoveryNodes(_source, _config);
if (isRefresh)
// 集群节点有更新过,重新执行
return this.DoExecute(slot, action, tryTimes);
else
// 集群节点未更新过,直接抛出异常
throw new RedisClusterException(ex.Message, c != null ? c.GetHostString() : string.Empty, ex);
}
}
finally
{
if (c != null)
c.Dispose();
}
} // 重新刷新集群信息
private bool DiscoveryNodes(IEnumerable<ClusterNode> source, RedisClientManagerConfig config)
{
bool lockTaken = false;
try
{
// noop
if (_isDiscoverying) { } Monitor.Enter(_objLock, ref lockTaken); _source = source;
_config = config;
_isDiscoverying = true; // 跟上次同步时间相隔 {MONITORINTERVAL} 秒钟以上才需要同步
if ((DateTime.Now - _lastDiscoveryTime).TotalMilliseconds >= MONITORINTERVAL)
{
bool isRefresh = false;
IList<InternalClusterNode> newNodes = RedisCluster.ReadClusterNodes(_source);
foreach (var node in newNodes)
{
var n = _clusterNodes.FirstOrDefault(x => x.HostString == node.HostString);
isRefresh =
n == null || // 新节点
n.Password != node.Password || // 密码变了
n.IsMater != node.IsMater || // 主变从或者从变主
n.IsSlave != node.IsSlave || // 主变从或者从变主
n.NodeFlag != node.NodeFlag || // 节点标记位变了
n.LinkState != node.LinkState || // 节点状态位变了
n.Slot.Start != node.Slot.Start || // 哈希槽变了
n.Slot.End != node.Slot.End || // 哈希槽变了
(n.RestSlots == null && node.RestSlots != null) ||
(n.RestSlots != null && node.RestSlots == null);
if (!isRefresh && n.RestSlots != null && node.RestSlots != null)
{
var slots1 = n.RestSlots.OrderBy(x => x.Start).ToList();
var slots2 = node.RestSlots.OrderBy(x => x.Start).ToList();
for (int index = ; index < slots1.Count; index++)
{
isRefresh =
slots1[index].Start != slots2[index].Start || // 哈希槽变了
slots1[index].End != slots2[index].End; // 哈希槽变了
if (isRefresh) break;
}
} if (isRefresh) break;
} if (isRefresh)
{
// 重新初始化集群
this.Dispose();
this.Initialize(newNodes);
this._lastDiscoveryTime = DateTime.Now;
}
} // 最后刷新时间在 {MONITORINTERVAL} 内,表示是最新群集信息 newest
return (DateTime.Now - _lastDiscoveryTime).TotalMilliseconds < MONITORINTERVAL;
}
finally
{
if (lockTaken)
{
_isDiscoverying = false;
Monitor.Exit(_objLock);
}
}
}

  六、配置访问组件调用入口

  最后我们需要为组件提供访问入口,我们用 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);

 /// <summary>
/// REDIS 集群工厂
/// </summary>
public class RedisClusterFactory
{
static RedisClusterFactory _factory = new RedisClusterFactory();
static RedisCluster _cluster = null; /// <summary>
/// Redis 集群
/// </summary>
public static RedisCluster Cluster
{
get
{
if (_cluster == null)
throw new Exception("You should call RedisClusterFactory.Configure to config cluster first.");
else
return _cluster;
}
} /// <summary>
/// 初始化 <see cref="RedisClusterFactory"/> 类的新实例
/// </summary>
private RedisClusterFactory()
{
} /// <summary>
/// 配置 REDIS 集群
/// <para>若群集中有指定 password 的节点,必须使用 IEnumerable&lt;ClusterNode&gt; 重载列举出这些节点</para>
/// </summary>
/// <param name="node">集群节点</param>
/// <returns></returns>
public static RedisCluster Configure(ClusterNode node)
{
return RedisClusterFactory.Configure(node, null);
} /// <summary>
/// 配置 REDIS 集群
/// <para>若群集中有指定 password 的节点,必须使用 IEnumerable&lt;ClusterNode&gt; 重载列举出这些节点</para>
/// </summary>
/// <param name="node">集群节点</param>
/// <param name="config"><see cref="RedisClientManagerConfig"/> 客户端缓冲池配置</param>
/// <returns></returns>
public static RedisCluster Configure(ClusterNode node, RedisClientManagerConfig config)
{
return RedisClusterFactory.Configure(new List<ClusterNode> { node }, config);
} /// <summary>
/// 配置 REDIS 集群
/// </summary>
/// <param name="nodes">集群节点</param>
/// <param name="config"><see cref="RedisClientManagerConfig"/> 客户端缓冲池配置</param>
/// <returns></returns>
public static RedisCluster Configure(IEnumerable<ClusterNode> nodes, RedisClientManagerConfig config)
{
if (nodes == null)
throw new ArgumentNullException("nodes"); if (nodes == null || nodes.Count() == )
throw new ArgumentException("There is no nodes to configure cluster."); if (_cluster == null)
{
lock (_factory)
{
if (_cluster == null)
{
RedisCluster c = new RedisCluster(nodes, config);
_cluster = c;
}
}
} return _cluster;
}
}

  总结

  今天我们详细介绍了如何从0手写一个Redis Cluster集群客户端访问组件,相信对同样在寻找类似解决方案的同学们会有一定的启发,喜欢的同学请点个 star。在没有相同案例可以参考的情况下笔者通过查阅官方说明文档和借鉴 Java 的 JedisCluster 的实现思路,虽说磕磕碰碰但最终也初步完成这个组件并投入使用,必须给自己加一个鸡腿!!在此我有一个小小的疑问,.NET 的同学们在用 Redis 集群时,你们是用什么组件耍的,为何网上的相关介绍和现成组件几乎都没有?欢迎讨论。

  GitHub 代码托管:https://github.com/TANZAME/ServiceStack.Redis.Cluster

  技术交流 QQ 群:816425449

【原创】强撸基于 .NET 的 Redis Cluster 集群访问组件的更多相关文章

  1. 借 redis cluster 集群,聊一聊集群中数据分布算法

    Redis Cluster 集群中涉及到了数据分布问题,因为 redis cluster 是多 master 的结构,每个 master 都是可以提供存储服务的,这就会涉及到数据分布的问题,在新的 r ...

  2. Redis Cluster集群搭建与应用

    1.redis-cluster设计 Redis集群搭建的方式有多种,例如使用zookeeper,但从redis 3.0之后版本支持redis-cluster集群,redis-cluster采用无中心结 ...

  3. 【精】搭建redis cluster集群,JedisCluster带密码访问【解决当中各种坑】!

    转: [精]搭建redis cluster集群,JedisCluster带密码访问[解决当中各种坑]! 2017年05月09日 00:13:18 冉椿林博客 阅读数:18208  版权声明:本文为博主 ...

  4. Redis Cluster集群知识学习总结

    Redis集群解决方案有两个: 1)  Twemproxy: 这是Twitter推出的解决方案,简单的说就是上层加个代理负责分发,属于client端集群方案,目前很多应用者都在采用的解决方案.Twem ...

  5. centos6下redis cluster集群部署过程

    一般来说,redis主从和mysql主从目的差不多,但redis主从配置很简单,主要在从节点配置文件指定主节点ip和端口,比如:slaveof 192.168.10.10 6379,然后启动主从,主从 ...

  6. Redis cluster集群:原理及搭建

    Redis cluster集群:原理及搭建 2018年03月19日 16:00:55 阅读数:6120 1.为什么使用redis? redis是一种典型的no-sql 即非关系数据库 像python的 ...

  7. Redis Cluster集群搭建<原>

    一.环境配置 一台window 7上安装虚拟机,虚拟机中安装的是centos系统. 二.目标     Redis集群搭建的方式有多种,根据集群逻辑的位置,大致可以分为三大类:基于客户端分片的Redis ...

  8. redis cluster 集群畅谈(三) 之 水平扩容、slave自动化迁移

    上一篇http://www.cnblogs.com/qinyujie/p/9029522.html, 主要讲解 实验多master写入.读写分离.实验自动故障切换(高可用性),那么本篇我们就来聊了聊r ...

  9. redis cluster集群部署

    上一篇http://www.cnblogs.com/qinyujie/p/9029153.html,主要讲解了 redis cluster 集群架构 的优势.redis cluster 和 redis ...

随机推荐

  1. DQN(Deep Q-learning)入门教程(三)之蒙特卡罗法算法与Q-learning算法

    蒙特卡罗法 在介绍Q-learing算法之前,我们还是对蒙特卡罗法(MC)进行一些介绍.MC方法是一种无模型(model-free)的强化学习方法,目标是得到最优的行为价值函数\(q_*\).在前面一 ...

  2. Qt版本中国象棋开发(三)

    实现功能:棋子初始化及走棋规则 棋子类: #ifndef STONE_H #define STONE_H #include <QString> class Stone { public: ...

  3. JVM调优总结(六)-新一代的垃圾回收算法

    垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要 ...

  4. AdaBoost理解

    AdaBoost是一种准确性很高的分类算法,它的原理是把K个弱分类器(弱分类器的意思是该分类器的准确性较低),通过一定的组合(一般是线性加权进行组合),组合成一个强的分类器,提高分类的准确性. 因此, ...

  5. vc程序设计--图形输出3

    // 实验2.cpp : 定义应用程序的入口点. // #include "framework.h" #include "实验2.h" #define MAX_ ...

  6. PAT1033 旧键盘打字 (20分) (关于测试点4超时问题)

    1033 旧键盘打字 (20分)   旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现.现在给出应该输入的一段文字.以及坏掉的那些键,打出的结果文字会是怎样? 输入格式: 输入在 2 ...

  7. Rocket - tilelink - WidthWidget

    https://mp.weixin.qq.com/s/pmJcsRMviJZjMwlwYw6OgA   简单介绍WidthWidget的实现.   ​​   1. 基本介绍   用于设定与上游节点连接 ...

  8. 八、【spring】web应用安全设计

    内容 Spring Security 使用Servlet规范中的Filter保护Web应用 基于数据库和LDAP进行认证 关键词 8.1 理解Spring Security模块 Spring Secu ...

  9. Linux磁盘与文件系统管理概要

    Linux磁盘与文件系统管理 硬盘组成与分区 硬盘组成 圆形的盘片(主要记录数据) 机械手臂与磁头(可读取盘片上的数据) 主轴马达,转动盘片,让机械手臂的磁头在盘片上读取数据 扇区(Sector)为最 ...

  10. Java实现 蓝桥杯 算法训练 数字三角形

    算法训练 数字三角形 时间限制:1.0s 内存限制:256.0MB 问题描述 (图3.1-1)示出了一个数字三角形. 请编一个程序计算从顶至底的某处的一条路 径,使该路径所经过的数字的总和最大. ●每 ...