Redis 数据倾斜?别慌!从成因到解决方案,一文帮你搞定
<p>本文已收录在Github,<strong>关注我,紧跟本系列专栏文章,咱们下篇再续!</strong></p><ul>
<li>🚀 魔都架构师 | 全网30W技术追随者</li>
<li>🔧 大厂分布式系统/数据中台实战专家</li>
<li>🏆 主导交易系统百万级流量调优 & 车联网平台架构</li>
<li>🧠 AIGC应用开发先行者 | 区块链落地实践者</li>
<li>🌍 以技术驱动创新,我们的征途是改变世界!</li>
<li>👉 实战干货:编程严选网</li>
</ul>
<p>分片集群中,数据会按一定分布规则分散到不同实例保存。<br>
如使用Redis Cluster,数据先CRC计算值对Slot(逻辑槽)取模,同时,所有Slot又会由运维管理员分配到不同实例。这样,数据就被保存到相应实例。这种方法实现简单,但易导致数据倾斜。</p>
<h2 id="1-数据倾斜">1 数据倾斜</h2>
<ul>
<li>数据量倾斜<br>
实例数据分布不均衡,某实例上的数据太多</li>
<li>数据访问倾斜<br>
虽然每个集群实例上的数据量差别不大,但某实例上的数据是热点数据,被访问得很频繁</li>
</ul>
<p>若数据倾斜了,则保存大量数据或保存热点数据的实例的处理压力就会增大,速度变慢,甚至可能引起该实例的内存耗尽。</p>
<h2 id="2-数据倾斜成因">2 数据倾斜成因</h2>
<p>数据倾斜时,数据在分片集群的多个实例上分布不均衡,大量数据集中到1或多个实例。</p>
<p>主要原因:某实例保存了bigkey、Slot分配不均衡及Hash Tag。</p>
<h3 id="21--bigkey导致倾斜">2.1bigkey导致倾斜</h3>
<h4 id="211-某实例正好保存bigkey">2.1.1 某实例正好保存bigkey</h4>
<p>大K的V值很大(String类型)或保存大量集合元素(集合类型),导致该实例的数据量增加,内存资源消耗严重。</p>
<p>大K操作一般都会造成实例I/O线程阻塞,若大K访问量较大,就会影响到这个实例上的其它请求的处理速度。</p>
<h4 id="212-应对方法">2.1.2 应对方法</h4>
<p>在业务层生成数据时,尽量避免将过多数据保存在同一KV对。<br>
若大K是集合类型,还能把大K拆分成很多个小的集合类型数据,分散保存在不同实例。</p>
<p>假设Hash类型集合<code>user:info</code>保存100万个用户信息,是个大K。<br>
则可按用户ID范围,将该集合拆成10个小集合,每个集合只保存10万个用户信息(如小集1保存ID1~10w的用户信息,小集2保存ID10w零 ~ 20w用户)。<br>
这就把一个大K化整为零、分散保存,避免大K给单切片实例带来访问压力。</p>
<p>大K访问量较大时,也会造成数据访问倾斜。</p>
<h3 id="22-slot分配不均衡导致倾斜">2.2 Slot分配不均衡导致倾斜</h3>
<p>若集群运维人员没有均衡地分配Slot,就会有大量数据被分配到同一Slot,而同一Slot只在一个实例上分布,导致大量数据被集中到一个实例。</p>
<p>假设Redis Cluster共16384个Slot,假设集群共5个实例,而实例1硬件配置较高,运维人员在给实例分配Slot时,就可能给实例1多分配Slot。</p>
<p>但我们不知道数据和Slot的对应关系,可能导致大量数据正好被映射到实例1的Slot,造成数据倾斜。</p>
<h4 id="221-应对">2.2.1 应对</h4>
<p>可通过运维规范,在分配前,就避免将过多Slot分配到同一实例。</p>
<p>已分配好Slot的集群,可先查看Slot和实例的具体分配关系,从而判断是否有过多Slot集中到同一实例:</p>
<ul>
<li>若有,就将部分Slot迁移到其它实例,避免数据倾斜。</li>
</ul>
<p>查看Slot分配情况:</p>
<ul>
<li>Redis Cluster,CLUSTER SLOTS命令</li>
<li>Codis,codis dashboard查看</li>
</ul>
<p>如执行CLUSTER SLOTS查看Slot分配情况:</p>
<ul>
<li>Slot 0 到Slot 4095被分配到了实例192.168.10.3</li>
<li>Slot 12288到Slot 16383被分配到了实例192.168.10.5</li>
</ul>
<pre><code class="language-shell">127.0.0.1:6379> cluster slots
1) 1) (integer) 0
2) (integer) 4095
3) 1) "192.168.10.3"
2) (integer) 6379
2) 1) (integer) 12288
2) (integer) 16383
3) 1) "192.168.10.5"
2) (integer) 6379
</code></pre>
<p>若某实例上有太多Slot,则可用迁移命令把这些Slot迁移到其它实例。<br>
Redis Cluster可使用3个命令完成Slot迁移:</p>
<ul>
<li>CLUSTER SETSLOT<br>
使用不同的选项进行三种设置,分别是设置Slot要迁入的目标实例,Slot要迁出的源实例,以及Slot所属的实例。</li>
<li>CLUSTER GETKEYSINSLOT<br>
获取某个Slot中一定数量的key。</li>
<li>MIGRATE<br>
把一个key从源实例实际迁移到目标实例。</li>
</ul>
<p>假设要把Slot 300从源实例(ID为3)迁移到目标实例(ID为5):</p>
<ul>
<li>先在目标实例5上执行命令,将Slot 300的源实例设置为实例3,表示要从实例3上迁入Slot 300。</li>
</ul>
<pre><code class="language-bash">CLUSTER SETSLOT 300 IMPORTING 3
</code></pre>
<ul>
<li>在源实例3上,我们把Slot 300的目标实例设置为5,这表示,Slot 300要迁出到实例5:</li>
</ul>
<pre><code>CLUSTER SETSLOT 300 MIGRATING 5
</code></pre>
<ul>
<li>从Slot 300中获取100 个key。因为Slot中的key数量可能很多,所以我们需要在客户端上多次执行下面的这条命令,分批次获得并迁移key。</li>
</ul>
<pre><code>CLUSTER GETKEYSINSLOT 300 100
</code></pre>
<ul>
<li>把刚才获取的100个key中的key1迁移到目标实例5上(IP为192.168.10.5),同时把要迁入的数据库设置为0号数据库,把迁移的超时时间设置为timeout。我们重复执行MIGRATE命令,把100个key都迁移完。</li>
</ul>
<pre><code>MIGRATE 192.168.10.5 6379 key1 0 timeout
</code></pre>
<ul>
<li>重复执行第3和第4步,直到Slot中的所有key都迁移完成。</li>
</ul>
<p>Redis 3.0.6开始,也可使用KEYS选项,一次迁移多个key(key1、2、3),提升迁移效率。</p>
<pre><code>MIGRATE 192.168.10.5 6379 "" 0 timeout KEYS key1 key2 key3
</code></pre>
<p>Codis可以执行下面的命令进行数据迁移。其中,我们把dashboard组件的连接地址设置为ADDR,并且把Slot 300迁移到编号为6的codis server group上。</p>
<pre><code>codis-admin --dashboard=ADDR -slot-action --create --sid=300 --gid=6
</code></pre>
<h3 id="23-hash-tag导致倾斜">2.3 Hash Tag导致倾斜</h3>
<p>加在KV对key中的一对花括号{},把K的一部分括起来,客户端在计算K的CRC16值时,只对Hash Tag花括号中的部分K内容进行计算。</p>
<p>不用Hash Tag,则客户端计算整个K的CRC16的值。</p>
<p>如K=<code>user:info:3231</code>,把3231作为Hash Tag,K就变成<code>user:info:{3231}</code>。客户端计算该K的CRC16,就只会计算3231的。否则会计算整个<code>user:info:3231</code>的CRC16值。</p>
<h4 id="231-好处">2.3.1 好处</h4>
<p>若不同K的Hash Tag内容相同,则这些K所对应数据会被映射到同一Slot,同时会被分配到同一实例。使用Hash Tag后,数据被映射到相同Slot的情况:</p>
<table>
<thead>
<tr>
<th>数据key</th>
<th>哈希计算</th>
<th>对应的Slot</th>
</tr>
</thead>
<tbody>
<tr>
<td 3231="">user:profile:</td>
<td>CRC16('3231') mod 16384</td>
<td>1024</td>
</tr>
<tr>
<td 5328="">user:profile:</td>
<td>CRC16('5328') mod 16384</td>
<td>3210</td>
</tr>
<tr>
<td 3231="">user:order:</td>
<td>CRC16('3231') mod 16384</td>
<td>1024</td>
</tr>
<tr>
<td 5328="">user:order:</td>
<td>CRC16('5328') mod 16384</td>
<td>3210</td>
</tr>
</tbody>
</table>
<p><code>user:profile:{3231}</code>和<code>user:order:{3231}</code>的Hash Tag一样,CRC16计算值对16384取模后的值也一样,所以映射到相同的1024Slot。</p>
<h4 id="232-应用场景">2.3.2 应用场景</h4>
<p>主要用在Redis Cluster,支持事务操作和范围查询。因为Redis Cluster本身不支持跨实例的事务操作和范围查询,当业务应用有这些需求时,就只能:</p>
<ul>
<li>先把这些数据读取到业务层进行事务处理</li>
<li>或逐个查询每个实例,得到范围查询的结果</li>
</ul>
<p>这很麻烦,所以,可使用Hash Tag把要执行事务操作或范围查询的数据映射到同一实例,就能轻松实现事务或范围查询。</p>
<h4 id="233-hash-tag潜在问题">2.3.3 Hash Tag潜在问题</h4>
<p>大量数据可能被集中到一个实例上,导致数据倾斜,集群负载不均衡。</p>
<h5 id="应对">应对</h5>
<p>需在范围查询、事务执行的需求和数据倾斜带来的访问压力之间,取舍!</p>
<h4 id="234-最佳实践">2.3.4 最佳实践</h4>
<p>若使用Hash Tag进行切片的数据会带来较大访问压力,优先考虑避免数据倾斜,最好不要使用Hash Tag进行数据切片。<br>
因为事务和范围查询都还可以放在客户端来执行,而数据倾斜会导致实例不稳定,造成服务不可用。</p>
<h2 id="3-数据访问倾斜的成因">3 数据访问倾斜的成因</h2>
<p>根因:实例上存在热点数据(如热点新闻内容、热销商品信息)。</p>
<p>一旦热点数据被存在某实例,则该实例的请求访问量就会远高于其它实例,访问压力很大:</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/other/1097393/202507/1097393-20250707111215898-1407740804.png" class="lazyload"></p>
<h3 id="31-应对">3.1 应对</h3>
<p>不同于数据量倾斜,热点数据通常是一或多个数据,所以,直接重新分配Slot也无法解决。</p>
<p>一般热点数据主要服务读操作,可使用热点数据多副本:把热点数据复制多份,在每个数据副本的key中增加一个随机前缀,使其和其它副本数据不会被映射到同一Slot。<br>
如此,热点数据既有多个副本可同时服务请求,这些副本数据的key又不同,会被映射到不同Slot。<br>
在给这些Slot分配实例时,注意把它们分到不同实例,则热点数据的访问压力就被分散到不同实例。</p>
<p>热点数据的多副本方法只针对只读热点数据。</p>
<p>若热点数据有读有写,就不适合了,因为要保证多副本间数据一致性。要给实例本身增加资源,如使用配置更高机器,应对大量访问压力。</p>
<h2 id="总结">总结</h2>
<p>数据倾斜的两种情况:</p>
<ul>
<li>数据量倾斜</li>
<li>数据访问倾斜</li>
</ul>
<p>导致数据量倾斜的主要原因:</p>
<ul>
<li>数据中有bigkey,导致某个实例的数据量增加</li>
<li>Slot手工分配不均,导致某个或某些实例上有大量数据</li>
<li>使用了Hash Tag,导致数据集中到某些实例上。</li>
</ul>
<p>数据访问倾斜主要原因:有热点数据存在,导致大量访问请求集中到了热点数据所在的实例上。</p>
<p>应对数据倾斜问题:</p>
<table>
<thead>
<tr>
<th>倾斜类型</th>
<th>倾斜成因</th>
<th>应对方法</th>
</tr>
</thead>
<tbody>
<tr>
<td>数据量倾斜</td>
<td>存在bigkey</td>
<td>业务层避免创建bigkey 把集合类型的bigkey拆分成多个小集合,分散保存</td>
</tr>
<tr>
<td>同上</td>
<td>Slot手工分配不均</td>
<td>制定运维规范,避免把过多Slot分配到一个实例上</td>
</tr>
<tr>
<td>同上</td>
<td>使用Hash Tag,导致大量数据集中到一个Slot</td>
<td>如果Hash Tag会造成数据倾斜,优先避免数据倾斜,不使用Hash Tag</td>
</tr>
<tr>
<td>数据访问倾斜</td>
<td>存在热点数据</td>
<td>采用带有不同key前缀的多副本方法</td>
</tr>
</tbody>
</table>
<p>Q:有数据访问倾斜时,若热点数据突然过期,而 Redis 中数据是缓存,数据的最终值保存在后端DB,会发生啥问题?</p>
<p>A:缓存击穿。Redis 很多性能问题,如导致 Redis 阻塞的场景:bigkey、集中过期、大实例 RDB等都与数据倾斜类似,都因数据集中、处理逻辑集中导致的耗时变长。解决思路类似,把集中变分散,如 bigkey 拆分为小 key、单个大实例拆分为分片集群等。</p>
<p>软件架构演进过程,从单机到分布式,再到MQ、负载均衡,都为将请求压力分散开,避免数据集中、请求集中,既能让系统承载更大的请求量,还保证系统稳定性。</p>
<blockquote>
<p>本文由博客一文多发平台 OpenWrite 发布!</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/JavaEdge/p/18970172
頁:
[1]