news 2026/4/24 12:26:17

Redis集群和典型应用场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis集群和典型应用场景

目录

集群(Cluster)

数据分片算法

哈希求余

一致性哈希算法

哈希槽分区算法(Redis使用)

使用docker搭建集群

故障处理(主机宕机)

集群扩容

Redis的典型应用-缓存

缓存定义

缓存更新策略

定期生成

实时生成

缓存预热(Cache preheating)

缓存穿透(Cache penetration)

缓存雪崩(Cache avalanche)

缓存击穿(Cache breakdown)

Redis的典型应用-分布式锁

分布式锁的概念

分布式锁的基本实现

引入过期时间

引入校验id

引入lua

引入watch dog(看门狗)

引入Redlcok算法


集群(Cluster)

上述的哨兵+主从复制解决的问题是“提高可用性”,不能解决“数据极端情况下丢失”的问题(写多份数据解决),也不能提高数据的存储容量,当我们需要存的数据接近或者超过机器的物理内存,哨兵+主从复制就难以胜任了(集群解决)。

Redis集群就是引入了多组master/slave,每一组master/slave存储数据全集的一部分,从而构成一个更大的整体,来提高数据的存储容量。

上图的每个Slave都是对应Master的备份。

每个红框都可以称为一个分片。如果全量数据进一步增加,只要再增加更多的分片,即可解决。不过随着分片的增多,存储的容量提升了,成本也变高了。

数据分片算法

Redis集群的核心思路是用多组机器来存储数据的每个部分,那么给定一个数据,如何知道这个数据应该存储在哪个分片上?读取的时候又应该去哪个分片读取?围绕这个问题业界有三种比较主流的实现方式。

哈希求余

设有N个分片,使用[0,N-1]这样的序号对其进行编号。针对某个给定的key,先计算其hash值(比如使用md5),再把得到的结果%N,得到的结果即为其对应分片的编号。(存储和读取的步骤都是这样)

优点:简单高效,数据分配均匀。

缺点:一旦需要扩容,N就改变了,原有的映射规则就被破坏了,就需要让节点之间的数据相互传输,重新排列,以满足新的映射关系。此时需要搬运的数据量是比较多的,开销很大。

一致性哈希算法

为了降低上述的搬运开销,能够更高效扩容,业界提出了“一致性哈希算法”。

key映射到分片序号的过程:

  1. 把0~2^32-1这个数据空间,映射到一个圆环上,数据按照顺时针方向增长。
  2. 假设当前存在三个分片,就把分片放到圆环的某个位置上。
  3. 假定有一个key,计算得到hash值H,从H所在位置顺时针往下找,找到的第一个分片,即为该key所从属的分片。这就相当于,N个分片的位置,把整个圆环分成了N个管辖区间,key的hash值落在某个区间内,就归对应区间管理。

在这个情况下,如果扩容一个分片执行的操作:原有的分片在环上的位置不动,只要再环上重新安排一个分片的位置即可。

优点:大大降低了扩容时数据搬运的规模,提高了扩容操作的效率。

缺点:数据分配不均匀(分片不一定刚好将圆环等分)。

哈希槽分区算法(Redis使用)

为了解决上述问题(搬运成本高和数据分配不均匀),Redis cluster引入了哈希槽算法。

hash_slot(哈希槽)=crc16(key)%16384 #crc16也是一种hash算法

将相当于是把整个哈希值,映射到16384个槽位上,也就是[0,16383]。然后再把这些槽位比较均匀的分配给每个分片。

假设当前有三个分片,一种可能得分配方式:

  • 0号分片:[0,5461],共5462个槽位。
  • 1号分片:[5462,10923],共5462个槽位。
  • 2号分片:[10924,16383],共5460个槽位。

每个分片的节点使用位图来表示自己持有哪些槽位,对于16384个槽位,需要2048个字节(2KB)大小的内存空间表示。

注意:每个分片持有的槽位不一定是连续的。

如果需要进行扩容一个分片,就可以对原有的槽位进行重新分配,一种可能得分配方式:

  • 0号分片:[0,4095],共4096个槽位。
  • 1号分片:[5462,9557],共4096个槽位。
  • 2号分片:[10924,15019],共4096个槽位。
  • 3号分片:[4096,5461]+[9558,10923]+[15019,16383],共4096个槽位。

虽然上述哈希槽的计算是%16384,但是实际上redis的作者建议集群分片数不应该超过1000个。因为分片数量过多,分片所包含的槽位就会非常少,就不能认为所包含的key数量是相当的,就会重现数据分配不均匀问题。

为什么是16384个槽位?

  • 节点之间通过心跳包通信,心跳包中包含了该节点有哪些slots,16384个槽位,需要位图的大小就是2KB,如果给定slots更多了,就需要消耗更多的空间。更大的空间对频繁的网络心跳包来说,还是一个不小的开销。
  • 另一方面,Redis集群一般建议超过1000个分片,所以16384对于最大1000分片来说已经足够用了。

使用docker搭建集群

第一步:创建目录和配置

创建redis-cluter目录,内部创建两个文件。

redis-cluster/

├── docker-compose.yml

└── generate.sh

generate.sh(shell脚本)内容:

for port in $(seq 1 9); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes #开启集群
cluster-config-file nodes.conf #nodes.conf集群配置信息,启动这些节点后会自动生成
cluster-node-timeout 5000 #多个节点之间心跳包通信的超时时间
cluster-announce-ip 172.30.0.10${port} #模拟的docker容器的ip
cluster-announce-port 6379 #绑定的容器内的业务端口
cluster-announce-bus-port 16379 #绑定容器内的管理端口
EOF
done

for port in $(seq 10 11); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done

执行脚本

bash generate.sh

第二步:编写docker-compose.yml

version: '3.7'
networks: #为了后续创建静态ip
mynet: #此时要先手动创建出网络,同时给合格网段也分配ip
ipam: #不能和你当前主机上现有的其他网段冲突
config:
- subnet: 172.30.0.0/24

services:
redis1:
image: 'redis:5.0.9'
container_name: redis1
restart: always
volumes:
- ./redis1/:/etc/redis/ #上述的shell脚本创建的目录,包含配置文件
ports: # 端口映射
- 6371:6379
- 16371:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.101 #此处配置静态ip
redis2:
image: 'redis:5.0.9'
container_name: redis2
restart: always
volumes:
- ./redis2/:/etc/redis/
ports:
- 6372:6379
- 16372:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.102
redis3:
image: 'redis:5.0.9'
container_name: redis3
restart: always
volumes:
- ./redis3/:/etc/redis/
ports:
- 6373:6379
- 16373:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.103
redis4:
image: 'redis:5.0.9'
container_name: redis4
restart: always
volumes:
- ./redis4/:/etc/redis/
ports:
- 6374:6379
- 16374:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.104
redis5:
image: 'redis:5.0.9'
container_name: redis5
restart: always
volumes:
- ./redis5/:/etc/redis/
ports:
- 6375:6379
- 16375:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.105
redis6:
image: 'redis:5.0.9'
container_name: redis6
restart: always
volumes:
- ./redis6/:/etc/redis/
ports:
- 6376:6379
- 16376:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.106
redis7:
image: 'redis:5.0.9'
container_name: redis7
restart: always
volumes:
- ./redis7/:/etc/redis/
ports:
- 6377:6379
- 16377:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.107

redis8:
image: 'redis:5.0.9'
container_name: redis8
restart: always
volumes:
- ./redis8/:/etc/redis/
ports:
- 6378:6379
- 16378:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.108
redis9:
image: 'redis:5.0.9'
container_name: redis9
restart: always
volumes:
- ./redis9/:/etc/redis/
ports:
- 6379:6379
- 16379:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.109

redis10:
image: 'redis:5.0.9'
container_name: redis10
restart: always
volumes:
- ./redis10/:/etc/redis/
ports:
- 6380:6379
- 16380:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.110
redis11:
image: 'redis:5.0.9'
container_name: redis11
restart: always
volumes:
- ./redis11/:/etc/redis/
ports:
- 6381:6379
- 16381:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.111

第三步:启动容器

docker-compose up -d

第四部:构建集群

此处是把前9个主机构建成集群,3主6从。

redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2

--cluster create表示建立集群,后面填写每个节点的ip地址和端口号。

--cluster-replicas 2表示每个主节点需要2个从节点进行备份。

执行之后,观察日志,可以看到哪些是主节点,哪些从节点跟随哪个主节点,以及主节点所持有的槽位号。

此时,使用客户端连上集群中的任何一个节点,都相当于脸上了整个集群。

redis-cli -h ip -p port 或 redis-cli -p port

这样使用客户端插入数据时,如果key没有落到当前节点的槽位上,就会发生报错,并告知该

key应该属于哪个分片。这时就必须要手动切换到这个分片,才能进行插入,这是非常低效的。

如果在连接时,后面加上-c选项,redis客户端就会根据当前key实际算出的槽位号,自动重定位到匹配的分片主机,完成操作。

redis-cli -p port -c

使用cluster nodes命令查看整个集群的情况。

故障处理(主机宕机)

手动停止一个master,模拟主节点宕机。

docker stop redis1

使用cluster nodes观察结果

可以看到101已经显示fail,然后原来是slave的105变成了新的master。和之前哨兵做的类似,会自动把宕机主机点所连接的从节点中,挑选一个成为主节点。

如果重新启动redis1

docker start redis1

观察结果,发现101变成了105的从节点。

故障处理流程

1)故障判定

  1. 节点A给节点B发送ping包,B就会给A返回⼀个pong包。ping和pong除了 message type 属性之外,其他部分都是⼀样的。这里包含了集群的配置信息。
  2. 每个节点,每秒会随机给一些节点发送ping包,而不是全发一遍,这样是为了避免在节点很多的时候,心跳包也非常多。
  3. 当节点A给B发起ping包,B不能如期回应的时候,此时A就会尝试重置和B的tcp连接,看能否连接成功。如果仍然连接失败,A就会把B设为PFAIL状态(相当于主观下线)。
  4. A判定B为PFAIL之后,会通过redsi内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态。
  5. 此时A发现其他很多节点,也认为B为PFAIL,并且数目超过总集群个数的一般,那么A就会把B标记为FAIL(客观下线),并且把这个消息同步给其他节点(其他节点收到之后,也会把B标记为FAIL)。

2)故障迁移

上述例子中,B故障了。如果B是从节点,就不需要进行故障迁移。如果B是主节点,那么就会由B的从节点(假设C和D)触发故障迁移。

  1. 从节点判定自己是否具有参选资格,如果从节点和主节点已经太久没通信(此时认为主从节点之间的数据差异太大了),时间超过阈值,就会失去竞争资格。
  2. 具有资格的节点,比如C和D,就会先休眠一定时间,休眠时间=500ms基础时间+[0,500ms]随机事件+排名*1000ms。offset的值越大,则排名越靠前。
  3. 比如C的休眠时间到了,C就会给其他所有集群中的节点,进行拉票操作。但是只有主节点才有投票的资格。
  4. 主节点就会把自己的票投给C(每个主节点只有1票)。但C收到的票数超过主节点数目的一半,C就会晋升成主节点(C自己负责执行slaveof no one,并且让D执行slaveof C)。
  5. 同时C还会把自己成为主节点的消息,同步给其他集群的节点。大家也都会更新自己保存的集群结构信息。

上述选举的过程,称为Raft算法。在随机休眠时间的加持下,基本上就是谁先唤醒,谁就能竞选成功。

某个或者某些节点宕机,有的时候会引起整个集群都宕机。

  • 某个分片,所有主从节点都挂了。
  • 某个分片,主节点挂了,但是没有从节点。
  • 超过半数的master节点都挂了。

集群扩容

集群扩容操作,是一件风险很高,成本很大的操作!!!

扩容操作:

第一步:把新的主节点加入到集群

redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

上述的命令,第一组地址表示的是新增节点的地址,第二组地址表示集群的任意一个节点的地址,代表要加入到哪个集群中。

第二步:重新分配slots

新的主节点加入到集群后,还没有被分配槽位,所以需要重新分配。

redis-cli --cluster reshard 172.30.0.101:6379

reshard后的地址是集群中的任意一个节点的地址,表示对哪个集群重新分配slots。

执行之后,会进入交互式操作,redis会提示用户输入一下内容:
1)多少个slots要进行重新分配?(此处因为是4个分片,所以填写4096)

2)哪个节点来接收这些slots?(此处填写新增的主节点的nodeID,通过cluster nodes查看)

3)这些slots从哪些节点搬运过来?

输入all表示从其他所有持有slots的主节点进行搬运;或者输入节点ID,以done结尾,手动指定从哪些节点搬运。

观察集群信息,发现新增节点已经被分配了slots。

在搬运key的过程中,对于那些不需要搬运的key,访问的时候是没有任何问题的。但是对于需要搬运的key,进行访问的时候可能会出现短暂的访问错误。随着搬运完成,这样的错误自然就恢复了。

第三步:给新的主节点添加从节点

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [前面新增主节点的nodeId]

观察集群信息,发现从节点添加成功。


Redis的典型应用-缓存

缓存定义

Redis 作为内存数据库,常用于缓存高频访问(热点)的数据,减少对后端数据库的直接访问压力。通过将数据存储在内存中,显著提升读取速度,适用于读多写少的场景。

缓存更新策略

定期生成

每隔一定的周期,对访问的数据频次进行统计,挑选出访问频次最高的前N%数据,通过定时任务或固定间隔更新缓存数据,适合数据变化不频繁的场景。例如,每日凌晨更新商品排行榜数据

优点:实现的过程比较简单,过程更可控,缓存中有啥比较固定,方便排查问题。

缺点:实时性不够,对于一些突发性事件的应对不行。如春节期间,“春晚”这样的词就会成为非常高频的词,而平时则很少会有人去搜。

实时生成

先给缓存设定容量上限(可以通过Redis配置文件的maxmemory参数设定)。

接下来把用户的每次查询:

  • 如果在Redis查到了,就直接返回。
  • 如果Redis中不存在,就从数据库查,把查到的结果同时也写入Redis。

如果缓存已经满了,就触发缓存淘汰策略,把一些相对不那么热门的数据淘汰掉。按照上述过程,持续一段时间之后Redis内部的数据自然就是热点数据了。

通用的淘汰策略

  • FIFO先进先出:把缓存中存在时间最久的(也就是最先来的数据)淘汰掉。
  • LRU淘汰最久未使用:记录每个key的最近访问时间,把最近访问时间最老的key淘汰掉。
  • LFU淘汰访问次数最少:记录每个key最近一段时间的访问次数,把访问次数最少得淘汰掉。
  • Random随机淘汰:从所有的key中随机抽取一个淘汰。

这里的淘汰策略,我们可以自己实现,也可以使用Redis提供的内置淘汰策略。

Redis内置的淘汰策略(在配置文件中配置)

  • volatile-lru :当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU算法进行淘汰。
  • allkeys-lru :当内存不足以容纳新写入数据时,从所有key中使用LRU算法进行淘汰。
  • volatile-lfu :4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中使用LFU算法进行淘汰。
  • allkeys-lfu :4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰.。
  • volatile-random :当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数 据。
  • allkeys-random :当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
  • volatile-ttl :在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰 (相当于FIFO,只不过是局限于过期的key)。
  • noeviction (默认策略):当内存不⾜以容纳新写入数据时,新写入操作会报错.

缓存预热(Cache preheating)

Redis服务器首次接入之后,服务器里是没有数据的。此时所有的请求都会打给数据库,使数据库承担巨大的压力。因此应对这种情况,就需要提前把热点数据准备好,直接写入到Redis中,帮助数据库承担压力。(上述的定期生成更新策略不涉及)

缓存穿透(Cache penetration)

访问的数据在Redis和数据库中都不存在。此时这样的数据就不会被更新到缓存上,后续如果仍然访问该数据,依然会访问数据库。这就会导致数据承担巨大的压力。

产生这种情况的原因

  • 业务设计不合理,比如缺少必要的参数校验环节,导致非法的数据也被查询了。
  • 开发/运维误操作,不小心把部分数据从数据库上误删了。
  • 黑客恶意攻击。

解决方案

  • 布隆过滤器拦截无效 Key
  • 针对数据库上不存在的key,也存储到Redis中,value设置成一个非法值(如"")。
  • 接口层针对要查询的参数进行严格的合法性校验。

缓存雪崩(Cache avalanche)

短时间内大量的key在缓存上失效,导致缓存命中率陡然下降,导致数据库压力骤增,甚至直接宕机。

产生这种情况的原因

  • Redis直接挂了。
  • Redis上的大量key同时过期。

解决方案

  • 部署高可用的Redis集群,并且完善监控报警体系。
  • 分散缓存过期时间(基础 TTL + 随机偏移值)。

缓存击穿(Cache breakdown)

相当于缓存雪崩的特殊情况。针对热点的key,突然失效了,导致大量的请求直接访问数据库上,甚至引起数据库宕机。

解决方案

  • 基于统计的方式发现热点key,并设置永不过期。
  • 进行必要的服务降级。例如访问数据库的时候使用分布式锁,限制同时请求数据库的并发数。

Redis的典型应用-分布式锁

分布式锁的概念

分布式锁是一种在分布式系统中协调多个进程或服务对共享资源进行互斥访问的机制。其核心目标是确保同一时刻只有一个客户端能持有锁,避免数据竞争或不一致问题。

分布式锁的基本实现

本质上就是通过一个键值对来标识锁的状态。比如,在买票服务器尝试买票,就需要先访问Redis,在Redis上设置一个键值对,key就是车次,value随便设置一个值。如果这个操作设置成功,就视为当前没有节点对该车次加锁,就可以进行数据库读写操作,操作完成之后,再把Redis上刚才的这个键值对给删除掉(解锁)。

Redis中提供的setnx命令,正好适合这个场景。

引入过期时间

为了避免因客户端故障导致的锁用久占用的问题。需要对锁设置过期时间。

使用set ex nx的命令,来设置锁的同把过期时间设置进去。如果将这个操作分开两个操作,就不能保证两个操作都一定能成功。

引入校验id

为锁分配唯一标识(如UUID),确保只有锁持有者能释放锁,避免被其他节点误删。

解锁逻辑伪代码

String key = [ 要加锁的资源 id];

String serverId = [ 服务器的编号 ];

// 加锁 , 设置过期时间为 10s

redis.set(key, serverId, "NX", "EX", "10s");

// 执⾏各种业务逻辑 , 比如修改数据库数据 .

doSomeThing();

// 解锁 , 删除 key. 但是删除前要检验下 serverId 是否匹配

if (redis.get(key) == serverId)

redis.del(key);

观察上述伪代码,可以发现解锁逻辑是分两步操作的,get和del,这样做并非是原子的。

引入lua

为了使解锁操作原子,可以使用Redis的脚本功能。

可以使用lua编写一些逻辑,把这个脚本传到redis服务器上,然后就可以让客户端控制redis执行上述脚本,redis执行脚本的过程也是原子的。

引入watch dog(看门狗)

上述方案仍然存在一个问题。当我们设置了key的过期时间之后,如果当前任务还能执行完,锁就先过期了,这就导致锁提前失效。

基于上述情况,就引入了watch dog。本质上是加锁服务器上的一个单独的线程(业务服务器上的),通过这个线程来对锁的过期时间进行“动态续约”。

举个例子:
初识情况下设置过期时间为10s,同时设定看门狗线程每隔3s检测一次。

那么当3s时间到的时候,看门狗就会判定当前任务是否完成。

  • 如果任务已经完成,则直接通过lua脚本的方式,释放锁。
  • 如果任务未完成,则把过期时间重新设置为10s。

引入Redlcok算法

实践中Redis一般是以集群的方式部署的。那么就看出现以下比较极端的情况。

服务器1向master节点尽心加锁操作,这个写入key的过程刚刚完成,master挂了;slave节 点升级成了新的master节点。但是由于刚才写入的这个key尚未来得及同步给slave呢,此时 就相当于服务器1的加锁操作形同虚设了,服务器2仍然可以进行加锁(即给新的master写 ⼊key,因为新的master不包含刚才的key).

为了解决这个问题,Redis引入了Redlcok算法。

引入⼀组Redis节点,其中每⼀组Redis节点都包含⼀个主节点和若干从节点。并且组和组之间存储的数据都是⼀致的,相互之间是"备份"关系。

加锁的时候,按照一定顺序,写多个master节点,在写锁的时候设定超时时间,如果加锁超过这个时间,就视为加锁失败。

如果给某个节点加锁失败,就立即尝试下一个节点加锁。当加锁成功的节点超过总节点数的一般,才视为加锁成功。这样的话,即使某些节点挂了,也不影响锁的正确性。

同理,释放锁的时候,也需要把所有的节点都进行解锁操作。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/24 12:25:20

抖音视频批量下载全攻略:高效实用的开源工具专业教程

抖音视频批量下载全攻略&#xff1a;高效实用的开源工具专业教程 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback suppor…

作者头像 李华
网站建设 2026/4/24 12:24:22

Zotero插件市场:5分钟打造你的专属学术工具箱终极指南

Zotero插件市场&#xff1a;5分钟打造你的专属学术工具箱终极指南 【免费下载链接】zotero-addons Zotero Add-on Market | Zotero插件市场 | Browsing, installing, and reviewing plugins within Zotero 项目地址: https://gitcode.com/gh_mirrors/zo/zotero-addons 还…

作者头像 李华
网站建设 2026/4/24 12:21:19

黑苹果休眠问题终极解决方案:从诊断到修复的完整指南

黑苹果休眠问题终极解决方案&#xff1a;从诊断到修复的完整指南 【免费下载链接】Hackintosh Hackintosh long-term maintenance model EFI and installation tutorial 项目地址: https://gitcode.com/gh_mirrors/ha/Hackintosh 黑苹果&#xff08;Hackintosh&#xff…

作者头像 李华
网站建设 2026/4/24 12:21:17

MathTranslate:精准翻译LaTeX学术论文的技术实践指南

MathTranslate&#xff1a;精准翻译LaTeX学术论文的技术实践指南 【免费下载链接】MathTranslate translate scientific papers in latex, especially arxiv papers 项目地址: https://gitcode.com/gh_mirrors/ma/MathTranslate 科研人员在处理国际文献时面临的核心难题…

作者头像 李华
网站建设 2026/4/24 12:19:17

如何用智能激活脚本轻松解决Windows和Office激活难题

如何用智能激活脚本轻松解决Windows和Office激活难题 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统频繁弹出激活提示而烦恼吗&#xff1f;Office文档突然变成只读模式让你束…

作者头像 李华
网站建设 2026/4/24 12:16:54

从TF-IDF到BM25:我的Elasticsearch搜索质量优化踩坑实录

从TF-IDF到BM25&#xff1a;我的Elasticsearch搜索质量优化踩坑实录 去年夏天&#xff0c;我们的电商平台突然收到大量用户投诉&#xff1a;"搜索蓝牙耳机为什么首页全是三年前的老款&#xff1f;"——这个看似简单的关键词匹配问题&#xff0c;却让我们技术团队经历…

作者头像 李华