redis笔记:分布式锁

本文介绍redis分布式的实现。

问题提出

现场反馈数据丢失问题。经查,是多进程使用文件锁的机制,在分布式环境下失效的原因导致的。当前机制是这样的,进程A获取lock文件的锁,成功后写A文件,不成功则等待;进程B获取lock文件锁,成功则将A文件重命名为B文件,无法获取则延后再尝试。通过重命名的方式,让A程序只写A文件,B程序只写B文件,2个进程使用同一文件锁实现互斥。

但在有多个节点的分布式环境中,并不能保证2个进程看到的都是同一个文件。重命名也不是原子操作,以下程序模拟打开文件并且不断写文件,但不关闭文件。

复制代码
$ cat write.py
import time

with open('/tmp/foo.txt', 'a') as file:
    while True:
        file.write('test...\n')
        file.flush()
        time.sleep(1)

具体测试过程:

1、运行python write.py不断写foo.txt。

2、再重命名:mv foo.txt bar.txt

3、查看新文件bar.txt,发现不断有数据产生。

查看2个文件的inode:

复制代码
$ ls -i foo.txt | awk '{print $1}'
71599628
$ mv foo.txt bar.txt
$ ls -i bar.txt | awk '{print $1}'
71599628

可以看到,两者的inode一样。

在 Linux 中,mv 命令仅修改文件名与 inode 的映射,不会影响已打开的文件句柄,因此,在重命名后,测试程序持续新文件写入,破坏了原有的数据逻辑。

解决方法

为此,需要寻求可用于分布式的锁机制,经查,redis是一个相对高效且简单的实现方案。

测试

测试环境

服务端:docker部署redis,单节点。

客户端:linux系统,redis命令行。

关键命令

如下表:

操作 指令 说明
加锁 SET lock_key unique_value NX PX 30000 NX = 仅当键不存在时设置,PX = 过期时间(30s),unique_value = 唯一标识(如 UUID)
解锁 Lua 脚本(原子判断 + 删除) 先判断 value 是否为自己的,再删除,避免误解锁
续期(可选) 定时刷新锁的过期时间(如每 10s 刷新为 30s) 防止锁在业务执行完前过期

连接:

复制代码
redis-cli -h 172.17.18.19 -p 6379 -a 123456

选择数据库:

复制代码
select 1

在一终端模拟程序A获取锁:

复制代码
set data:lock client_aaa_123 NX PX 20000

在同一终端或在另一终端模拟程序B获取锁:

复制代码
set data:lock client_bbb_123 NX PX 30000

查看存活时间(秒):

复制代码
TTL data:lock

查看锁:

复制代码
get data:lock

安全删除锁(使用Lua脚本确保原子性,即先检查值再删除),程序A:

复制代码
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 data:lock "client_aaa_123"

安全删除锁,程序B:

复制代码
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 data:lock "client_bbb_123"

不正确的删除锁:

复制代码
del data:lock

判断锁是否存在:

复制代码
exists data:lock

命令行测试

场景1:程序A先获取锁,30秒后,程序B获取锁。

结论:30秒后,锁被释放,B正常获取锁。

复制代码
A获取锁成功:
> set data:lock client_aaa_123 NX PX 30000
OK

在30秒内,程序B获取锁失败:
> set data:lock client_bbb_123 NX PX 30000
(nil)

30秒超时后,存活时间为负数,即已超时被释放
> ttl  data:lock
(integer) -2

程序B获取锁成功:
> set data:lock client_bbb_123 NX PX 30000
OK

场景2:程序A先获取锁,处理完毕后释放,程序B获取锁

结论:锁被所有者A释放,B正常获取锁。

复制代码
A获取锁成功:
> set data:lock client_aaa_123 NX PX 30000
OK

在存活期内程序A释放锁:
> ttl  data:lock
(integer) 17
> EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 data:lock "client_aaa_123"
(integer) 1

存活时间为负数,即已超时被释放
> ttl  data:lock
(integer) -2

程序B获取锁成功:
> set data:lock client_bbb_123 NX PX 30000
OK

golang实现

核心代码

将redis相关操作封装成工具函数,再新加分布式锁功能,代码如下:

复制代码
package util

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/go-redis/redis/v8"
)

// RedisClient 多DB连接池客户端
type RedisClient struct {
	clients map[int]*redis.Client // DB编号到连接的映射
	mu      sync.RWMutex          // 并发控制锁
	config  *RedisConfig          // 基础配置
	baseCtx context.Context       // 基础上下文
	cancel  context.CancelFunc    // 整体取消函数

	lock *RedisLock // 分布式锁配置
}

// 分布式锁结构体
type RedisLock struct {
	lockKey    string // 锁名
	uniqueID   string // 唯一标识(防止误解锁)
	expireTime int    // 锁过期时间,单位为秒
}

// RedisConfig Redis连接配置
type RedisConfig struct {
	Addr     string // 地址,格式: host:port
	Password string // 密码
	DB       int    // 默认数据库编号
	PoolSize int    // 每个DB的连接池大小
	Timeout  int
}

// NewRedisClient 创建多DB连接池
func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
	if config == nil {
		return nil, errors.New("redis config cannot be nil")
	}

	// 设置默认值
	if config.PoolSize <= 0 {
		config.PoolSize = 10
	}
	if config.Timeout <= 0 {
		config.Timeout = 3
	}

	// 创建基础上下文,用于整个客户端的生命周期
	ctx, cancel := context.WithCancel(context.Background())

	rc := &RedisClient{
		clients: make(map[int]*redis.Client),
		config:  config,
		baseCtx: ctx,
		cancel:  cancel,
	}

	// 初始化默认DB连接
	if _, err := rc.getDBClient(config.DB); err != nil {
		return nil, err
	}

	return rc, nil
}

// getDBClient 获取指定DB的连接(线程安全)
func (rc *RedisClient) getDBClient(db int) (*redis.Client, error) {
	// 读锁检查现有连接
	rc.mu.RLock()
	if client, ok := rc.clients[db]; ok {
		rc.mu.RUnlock()
		return client, nil
	}
	rc.mu.RUnlock()

	// 写锁创建新连接
	rc.mu.Lock()
	defer rc.mu.Unlock()

	// 双检锁再次检查
	if client, ok := rc.clients[db]; ok {
		return client, nil
	}

	dialTimeout := time.Duration(rc.config.Timeout) * time.Second

	// 创建新连接
	client := redis.NewClient(&redis.Options{
		Addr:        rc.config.Addr,
		Password:    rc.config.Password,
		DB:          db,
		PoolSize:    rc.config.PoolSize,
		DialTimeout: dialTimeout,
	})

	// 测试连接(指定超时时间)
	ctx, cancel := context.WithTimeout(rc.baseCtx, dialTimeout)
	defer cancel()
	if _, err := client.Ping(ctx).Result(); err != nil {
		return nil, fmt.Errorf("failed to connect DB %d: %w", db, err)
	}

	rc.clients[db] = client
	return client, nil
}

// Close 关闭所有DB连接
func (rc *RedisClient) Close() error {
	rc.cancel()

	rc.mu.Lock()
	defer rc.mu.Unlock()

	var errs []error
	for db, client := range rc.clients {
		if err := client.Close(); err != nil {
			errs = append(errs, fmt.Errorf("DB %d: %w", db, err))
		}
		delete(rc.clients, db)
	}

	if len(errs) > 0 {
		return fmt.Errorf("errors closing connections: %v", errs)
	}
	return nil
}

// HealthCheck 检查所有DB连接状态
func (rc *RedisClient) HealthCheck() map[int]bool {
	rc.mu.RLock()
	defer rc.mu.RUnlock()

	status := make(map[int]bool)
	for db, client := range rc.clients {
		ctx, cancel := context.WithTimeout(rc.baseCtx, time.Second)
		_, err := client.Ping(ctx).Result()
		cancel()
		status[db] = err == nil
	}
	return status
}

// --- 基础操作封装(自动使用默认DB)---

func (rc *RedisClient) Set(key string, value interface{}, second int) error {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return err
	}
	return client.Set(rc.baseCtx, key, value, time.Duration(second)*time.Second).Err()
}

func (rc *RedisClient) Get(key string) (string, error) {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return "", err
	}
	return client.Get(rc.baseCtx, key).Result()
}

// --- 指定DB操作 ---

func (rc *RedisClient) SetWithDB(db int, key string, value interface{}, second int) error {
	client, err := rc.getDBClient(db)
	if err != nil {
		return err
	}
	return client.Set(rc.baseCtx, key, value, time.Duration(second)*time.Second).Err()
}

func (rc *RedisClient) GetWithDB(db int, key string) (string, error) {
	client, err := rc.getDBClient(db)
	if err != nil {
		return "", err
	}
	return client.Get(rc.baseCtx, key).Result()
}

// --- 高级用法 ---

// Do 在指定DB执行自定义操作
func (rc *RedisClient) Do(db int, fn func(*redis.Client) error) error {
	client, err := rc.getDBClient(db)
	if err != nil {
		return err
	}
	return fn(client)
}

// GetClients 获取当前所有活跃的DB连接(只读)
func (rc *RedisClient) GetClients() map[int]*redis.Client {
	rc.mu.RLock()
	defer rc.mu.RUnlock()

	copy := make(map[int]*redis.Client)
	for k, v := range rc.clients {
		copy[k] = v
	}
	return copy
}

// --- 哈希操作 ---

// HSet 设置哈希字段值
func (rc *RedisClient) HSet(key string, values ...interface{}) error {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return err
	}
	return client.HSet(rc.baseCtx, key, values...).Err()
}

// HGet 获取哈希字段值
func (rc *RedisClient) HGet(key, field string) (string, error) {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return "", err
	}
	return client.HGet(rc.baseCtx, key, field).Result()
}

// HGetAll 获取所有哈希字段和值
func (rc *RedisClient) HGetAll(key string) (map[string]string, error) {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return map[string]string{}, err
	}
	return client.HGetAll(rc.baseCtx, key).Result()
}

// --- 其他实用功能 ---

// Ping 测试连接是否正常
func (rc *RedisClient) Ping() (string, error) {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return "", err
	}

	ctx, cancel := context.WithTimeout(rc.baseCtx, time.Second)
	defer cancel()

	return client.Ping(ctx).Result()
}

// TTL 获取键的剩余生存时间
func (rc *RedisClient) TTL(key string) (time.Duration, error) {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return 0, err
	}
	return client.TTL(rc.baseCtx, key).Result()
}

// Keys 查找所有符合给定模式的键
func (rc *RedisClient) Keys(pattern string) ([]string, error) {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return []string{}, err
	}
	return client.Keys(rc.baseCtx, pattern).Result()
}

// FlushDB 清空当前数据库
func (rc *RedisClient) FlushDB() error {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return err
	}
	return client.FlushDB(rc.baseCtx).Err()
}

// FlushAll 清空所有数据库
func (rc *RedisClient) FlushAll() error {
	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return err
	}
	return client.FlushAll(rc.baseCtx).Err()
}

///////////////////////////////

// 新建分布式锁
func (rc *RedisClient) SetRedisLock(lockKey, lockValue string, expireTime int) {
	foo := RedisLock{
		lockKey:    lockKey,
		uniqueID:   lockValue, // 生成唯一ID
		expireTime: expireTime,
	}
	rc.lock = &foo
}

// Lock 加锁(原子操作)
func (rc *RedisClient) Lock() (bool, error) {
	var ok bool = false

	if rc.lock == nil {
		return ok, fmt.Errorf("锁配置为空")
	}

	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return ok, err
	}

	expireTime := time.Duration(rc.lock.expireTime) * time.Second
	// SETNX + PX 原子加锁
	ok, err = client.SetNX(rc.baseCtx, rc.lock.lockKey, rc.lock.uniqueID, expireTime).Result()
	if err != nil {
		return ok, fmt.Errorf("加锁失败: %v", err)
	}
	return ok, nil
}

// UnLock 解锁(原子操作) 注:当TTL过期后,UnLock返回0
func (rc *RedisClient) UnLock() (int64, error) {
	var ret int64 = 0
	if rc.lock == nil {
		return ret, fmt.Errorf("锁配置为空")
	}

	client, err := rc.getDBClient(rc.config.DB)
	if err != nil {
		return ret, err
	}

	// Lua脚本:先判断value是否匹配,再删除
	unlockScript := `
		if redis.call('GET', KEYS[1]) == ARGV[1] then
			return redis.call('DEL', KEYS[1])
		else
			return 0
		end
	`
	ret, err = client.Eval(rc.baseCtx, unlockScript, []string{rc.lock.lockKey}, rc.lock.uniqueID).Int64()
	if err != nil {
		return ret, fmt.Errorf("解锁失败: %v", err)
	}
	return ret, nil
}

测试

测试代码:

复制代码
var lockkey string = "data:lock"
var lockvalue string
var ttlTime int = 5 //锁过期时间

func Test(argv []string) (ret string) {
	klog.Println("test of redis lock")

	if len(argv) < 2 {
		klog.Println("需再加一个参数作为value\n")
		return
	}

	lockvalue = argv[1]

	rclient, err := InitRedisLock("172.16.9.19:16379", "Gxjky@2020", 1, 3, 30)
	if err != nil {
		klog.Println(err)
		return
	}
	// 指定key和value,过期时间
	klog.Printf("设置锁参数 key: %v value: %v ttl: %v\n", lockkey, lockvalue, ttlTime)
	rclient.SetRedisLock(lockkey, lockvalue, ttlTime)

	go func() {
		for {
			ttl, _ := rclient.TTL(lockkey)
			value, _ := rclient.Get(lockkey)
			klog.Printf("监控 key: %v value: %v ttl: %v\n", lockkey, value, ttl)
			com.Sleep(1000)
		}
	}()


	for {
		ok, err := rclient.Lock()

		if !ok {
			value, _ := rclient.Get(lockkey)
			klog.Printf("无法获取锁(当前值: %v),等待2秒\n", value)

			// com.Sleep(com.GetRandomNum(100, 200))
			com.Sleep(2000)
			continue
		}

		klog.Println("成功获取到锁,Lock返回:", ok, err)

		s := com.GetRandomNum(1, 4) // 模拟的耗时,要小于TTL,当TTL过期后,UnLock返回0
		klog.Printf("假装在处理业务,耗时 %v 秒\n", s)
		com.Sleep(s * 1000)

		ret, err := rclient.UnLock()

		s = com.GetRandomNum(3, 7)
		klog.Printf("UnLock返回:%v %v,延时 %v 秒\n", ret, err, s)

		com.Sleep(s * 1000)
	}
}

func InitRedisLock(addr, password string, db int, timeout, ttl int) (rc *util.RedisClient, err error) {
	err = nil

	config := &util.RedisConfig{
		Addr:     addr,
		Password: password,
		DB:       db,
		Timeout:  timeout,
	}

	client, err1 := util.NewRedisClient(config)
	if err1 != nil {
		err = err1
		return
	}

	rc = client

	return
}

场景1:程序A获取锁,超时后,才能获取锁

复制代码
$ ./cmdHub-win.exe -m misc redislock client_aaa_123

[2026-01-30 19:50:53 949] [INFO] test of redis lock
[2026-01-30 19:50:53 958] [INFO] 设置锁参数 key: data:lock value: client_aaa_123 ttl: 6
[2026-01-30 19:50:53 962] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:50:53 962] [INFO] 假装在处理业务,耗时2秒
[2026-01-30 19:50:53 967] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 6s
[2026-01-30 19:50:54 973] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 5s
[2026-01-30 19:50:55 968] [INFO] 无法获取锁,等待2秒
[2026-01-30 19:50:55 979] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 4s
[2026-01-30 19:50:56 986] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 3s
[2026-01-30 19:50:57 978] [INFO] 无法获取锁,等待2秒
[2026-01-30 19:50:57 995] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 2s
[2026-01-30 19:50:59 004] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 1s
[2026-01-30 19:50:59 989] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:50:59 989] [INFO] 假装在处理业务,耗时2秒
[2026-01-30 20:10:00 010] [INFO] 监控 key: data:lock value: client_aaa_123 ttl: 6s

从日志看到,不手动释放锁情况下,需在超时后才能再次获取锁。从监控日志看,TTL不断在倒计时。

场景2:程序A获取锁,观察超时后释放锁的状态

测试代码:

复制代码
func UnLockAfterTime(rclient *util.RedisClient) {
	ok, err := rclient.Lock()
	if !ok {
		value, _ := rclient.Get(lockkey)
		klog.Printf("无法获取锁(当前值: %v),等待2秒\n", value)
		return
	}

	klog.Println("成功获取到锁,Lock返回:", ok, err)

	s := com.GetRandomNum(3, 8) // 模拟的耗时,要小于TTL,当TTL过期后,UnLock返回0
	klog.Printf("假装在处理业务,耗时 %v 秒\n", s)
	com.Sleep(s * 1000)

	ret, err := rclient.UnLock()

	klog.Printf("UnLock返回:%v %v\n", ret, err)
}

输出日志:

复制代码
[2026-01-30 20:14:13 304] [INFO] test of redis lock
[2026-01-30 20:14:13 313] [INFO] 设置锁参数 key: data:lock value: client_aaa_123 ttl: 5
[2026-01-30 20:14:13 317] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 20:14:13 317] [INFO] 假装在处理业务,耗时 3 秒
[2026-01-30 20:14:16 321] [INFO] UnLock返回:1 <nil>
[2026-01-30 20:14:18 332] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 20:14:18 332] [INFO] 假装在处理业务,耗时 3 秒
[2026-01-30 20:14:21 338] [INFO] UnLock返回:1 <nil>
[2026-01-30 20:14:23 340] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 20:14:23 341] [INFO] 假装在处理业务,耗时 8 秒
[2026-01-30 20:14:31 344] [INFO] UnLock返回:0 <nil>
[2026-01-30 20:14:33 347] [INFO] 成功获取到锁,Lock返回: true <nil>

场景3:程序A、B同时同时执行,观察2者获取锁的过程

先结合日志分析锁占用释放过程:

  • 19:30:30 908:程序A获取到锁后,业务处理完毕,释放锁。延时7秒再开始下一轮处理。
  • 19:30:37 349:程序B执行,获取到锁(比程序A提前约600毫秒),占用3秒。
  • 19:30:37 920:紧接着,程序A执行。此时redis对应value为client_bbb_123,说明B在占用,还没释放,无法获取到锁,符合预期。
  • 19:30:40 353:程序B处理3秒时间到,释放锁。延时4秒再开始下一轮处理。
  • 19:30:41 928:程序A执行(B还在休息),获取到锁。业务耗时3秒。
  • 19:30:44 359:程序B执行,间隔不到3秒,此时redis对应value为client_aaa_123,说明A在占用,还没释放。

程序A日志:

复制代码
./cmdHub-win.exe -m misc redislock client_aaa_123

[2026-01-30 19:30:28 892] [INFO] test of redis lock
[2026-01-30 19:30:28 901] [INFO] 设置锁参数 key: data:lock value: client_aaa_123 ttl: 6
[2026-01-30 19:30:28 904] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:28 904] [INFO] 假装在处理业务,耗时 2 秒
[2026-01-30 19:30:30 908] [INFO] UnLock返回:1 <nil>,延时 7 秒
[2026-01-30 19:30:37 920] [INFO] 无法获取锁(当前值: client_bbb_123),等待2秒
[2026-01-30 19:30:39 926] [INFO] 无法获取锁(当前值: client_bbb_123),等待2秒
[2026-01-30 19:30:41 928] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:41 928] [INFO] 假装在处理业务,耗时 3 秒
[2026-01-30 19:30:44 932] [INFO] UnLock返回:1 <nil>,延时 6 秒
[2026-01-30 19:30:50 934] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:50 935] [INFO] 假装在处理业务,耗时 2 秒
[2026-01-30 19:30:52 946] [INFO] UnLock返回:1 <nil>,延时 6 秒
[2026-01-30 19:30:58 949] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:58 949] [INFO] 假装在处理业务,耗时 1 秒
[2026-01-30 19:30:59 952] [INFO] UnLock返回:1 <nil>,延时 5 秒

程序B日志:

复制代码
$ ./cmdHub-win.exe -m misc redislock client_bbb_123
[2026-01-30 19:30:32 321] [INFO] test of redis lock
[2026-01-30 19:30:32 330] [INFO] 设置锁参数 key: data:lock value: client_bbb_123 ttl: 6
[2026-01-30 19:30:32 332] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:32 333] [INFO] 假装在处理业务,耗时 1 秒
[2026-01-30 19:30:33 337] [INFO] UnLock返回:1 <nil>,延时 4 秒
[2026-01-30 19:30:37 349] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:37 349] [INFO] 假装在处理业务,耗时 3 秒
[2026-01-30 19:30:40 353] [INFO] UnLock返回:1 <nil>,延时 4 秒
[2026-01-30 19:30:44 359] [INFO] 无法获取锁(当前值: client_aaa_123),等待2秒
[2026-01-30 19:30:46 363] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:46 363] [INFO] 假装在处理业务,耗时 3 秒
[2026-01-30 19:30:49 367] [INFO] UnLock返回:1 <nil>,延时 3 秒
[2026-01-30 19:30:52 373] [INFO] 无法获取锁(当前值: client_aaa_123),等待2秒
[2026-01-30 19:30:54 377] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:30:54 377] [INFO] 假装在处理业务,耗时 2 秒
[2026-01-30 19:30:56 382] [INFO] UnLock返回:1 <nil>,延时 3 秒
[2026-01-30 19:30:59 387] [INFO] 无法获取锁(当前值: client_aaa_123),等待2秒
[2026-01-30 19:31:01 391] [INFO] 成功获取到锁,Lock返回: true <nil>
[2026-01-30 19:31:01 391] [INFO] 假装在处理业务,耗时 3 秒

小结

从模拟测试结果可知,使用redis分布式锁,能让不同的客户端程序实现互斥访问,各客户端使用不同的value,可以用程序名称+程序启动时间戳,也可再加上IP和版本号,从而实现自己的锁自己解,加了超时时间,能够避免死锁的情况产生。

相关推荐
heartbeat..3 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
Prince-Peng3 小时前
技术架构系列 - 详解Redis
数据结构·数据库·redis·分布式·缓存·中间件·架构
虾说羊3 小时前
redis中的哨兵机制
数据库·redis·缓存
ruxshui3 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
慾玄3 小时前
渗透笔记总结
笔记
CS创新实验室4 小时前
关于 Moltbot 的学习总结笔记
笔记·学习·clawdbot·molbot
醒过来摸鱼4 小时前
Redis 服务器线程与事件循环解析
服务器·数据库·redis
曹天骄5 小时前
基于 Cloudflare Worker + KV 构建高性能分布式测速调度系统(工程实战)
分布式
虾说羊5 小时前
redis中的容灾机制
java·数据库·redis
春生野草5 小时前
Redis安装与使用(Linux)
linux·redis