【原创】强撸基于 .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客户端,需要实现以下几个要点:
如下面类图所示,接下来我们详细分析具体的代码实现。
一、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
:逗号列表分隔的标志:myself
,master
,slave
,fail?
,fail
,handshake
,noaddr
,noflags
。标志在下一节详细解释。
4. master
:如果节点是从属节点,并且主节点已知,则节点ID为主节点,否则为“ - ”字符。
5. ping-sent
:以毫秒为单位的当前激活的ping发送的unix时间,如果没有挂起的ping,则为零。
6. pong-recv
:毫秒 unix 时间收到最后一个乒乓球。
7. config-epoch
:当前节点(或当前主节点,如果该节点是从节点)的配置时期(或版本)。每次发生故障切换时,都会创建一个新的,唯一的,单调递增的配置时期。如果多个节点声称服务于相同的哈希槽,则具有较高配置时期的节点将获胜。
8. link-state
:用于节点到节点集群总线的链路状态。我们使用此链接与节点进行通信。可以是connected
或disconnected
。
9. slot
:散列槽号或范围。从参数9开始,但总共可能有16384个条目(限制从未达到)。这是此节点提供的散列槽列表。如果条目仅仅是一个数字,则被解析为这样。如果它是一个范围,它是在形式start-end
,并且意味着节点负责所有散列时隙从start
到end
包括起始和结束值。
标志的含义(字段编号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<ClusterNode> 重载列举出这些节点</para>
/// </summary>
/// <param name="node">集群节点</param>
/// <returns></returns>
public static RedisCluster Configure(ClusterNode node)
{
return RedisClusterFactory.Configure(node, null);
} /// <summary>
/// 配置 REDIS 集群
/// <para>若群集中有指定 password 的节点,必须使用 IEnumerable<ClusterNode> 重载列举出这些节点</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 集群访问组件的更多相关文章
- 借 redis cluster 集群,聊一聊集群中数据分布算法
Redis Cluster 集群中涉及到了数据分布问题,因为 redis cluster 是多 master 的结构,每个 master 都是可以提供存储服务的,这就会涉及到数据分布的问题,在新的 r ...
- Redis Cluster集群搭建与应用
1.redis-cluster设计 Redis集群搭建的方式有多种,例如使用zookeeper,但从redis 3.0之后版本支持redis-cluster集群,redis-cluster采用无中心结 ...
- 【精】搭建redis cluster集群,JedisCluster带密码访问【解决当中各种坑】!
转: [精]搭建redis cluster集群,JedisCluster带密码访问[解决当中各种坑]! 2017年05月09日 00:13:18 冉椿林博客 阅读数:18208 版权声明:本文为博主 ...
- Redis Cluster集群知识学习总结
Redis集群解决方案有两个: 1) Twemproxy: 这是Twitter推出的解决方案,简单的说就是上层加个代理负责分发,属于client端集群方案,目前很多应用者都在采用的解决方案.Twem ...
- centos6下redis cluster集群部署过程
一般来说,redis主从和mysql主从目的差不多,但redis主从配置很简单,主要在从节点配置文件指定主节点ip和端口,比如:slaveof 192.168.10.10 6379,然后启动主从,主从 ...
- Redis cluster集群:原理及搭建
Redis cluster集群:原理及搭建 2018年03月19日 16:00:55 阅读数:6120 1.为什么使用redis? redis是一种典型的no-sql 即非关系数据库 像python的 ...
- Redis Cluster集群搭建<原>
一.环境配置 一台window 7上安装虚拟机,虚拟机中安装的是centos系统. 二.目标 Redis集群搭建的方式有多种,根据集群逻辑的位置,大致可以分为三大类:基于客户端分片的Redis ...
- redis cluster 集群畅谈(三) 之 水平扩容、slave自动化迁移
上一篇http://www.cnblogs.com/qinyujie/p/9029522.html, 主要讲解 实验多master写入.读写分离.实验自动故障切换(高可用性),那么本篇我们就来聊了聊r ...
- redis cluster集群部署
上一篇http://www.cnblogs.com/qinyujie/p/9029153.html,主要讲解了 redis cluster 集群架构 的优势.redis cluster 和 redis ...
随机推荐
- Nginx 的过滤模块是干啥用的?
上一篇文章我写了 Nginx 的 11 个阶段,很多人都说太长了.这是出于文章完整性的考虑的,11 个阶段嘛,一次性说完就完事了.今天这篇文章比较短,看完没问题. 过滤模块的位置 之前我们介绍了 Ng ...
- 「持续集成实践系列」Jenkins 2.x 搭建CI需要掌握的硬核要点
1. 前言 随着互联网软件行业快速发展,为了抢占市场先机,企业不得不持续提高软件的交付效率.特别是现在国内越来越多企业已经在逐步引入DevOps研发模式的变迁,在这些背景催促之下,对于企业研发团队所需 ...
- Linux学习(一):常用命令
init 0:关机 init 3:命令行模式 init 5:图形界面模式 init 6:重启 shutdown -h now:立马关机 ls:文件列表 参数:-l 详细列表 cd:切换目录 用法实例: ...
- Spring IoC componet-scan 节点解析详解
前言 我们在了解 Spring 容器的扩展功能 (ApplicationContext) 之前,先介绍下 context:componet-scan 标签的解析过程,其作用很大是注解能生效的关键所在. ...
- word dde payload
payload: ctrl+F9 {DDEAUTO c:\\windows\\system32\\cmd.exe "/k calc.exe" } Since this techni ...
- 循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理
在我们一般开发的系统界面里面,列表页面是一个非常重要的综合展示界面,包括有条件查询.列表展示和分页处理,以及对每项列表内容可能进行的转义处理,本篇随笔介绍基于Vue +Element基础上实现表格列表 ...
- 深入了解ConcurrentHashMap
在上一篇文章[简单了解系列]从基础的使用来深挖HashMap里,我从最基础的使用中介绍了HashMap,大致是JDK1.7和1.8中底层实现的变化,和介绍了为什么在多线程下可能会造成死循环,扩容机智是 ...
- IntelliJ IDEA连接不上数据库 (Connection to testdb@localhost failed. [08001] Could not create connection to database server. Attempted reconnect 3 times. Giving up.)
问题提示为: 原因:MySQL数据库版本为8.0以上,需要在URL加上时区,即加上?serverTimezone=GMT 成功后为:
- MySQL 高级—— Join 、索引 、优化
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.Join 查询 1.SQL执行顺序(一般情况下) 1.1 手写顺序: SELECT DISTINCT ...
- Java实现 LeetCode 232 用栈实现队列
232. 用栈实现队列 使用栈实现队列的下列操作: push(x) – 将一个元素放入队列的尾部. pop() – 从队列首部移除元素. peek() – 返回队列首部的元素. empty() – 返 ...