Redis String 类型的底层实现与性能优化

Redis String 是 Redis 中最基础也是应用最广泛的数据类型,它能存储文本、数字、二进制数据等多种形式的信息。深入理解其底层实现对构建高性能分布式系统至关重要。

Redis 字符串的底层结构:SDS

Redis 没有使用 C 语言原生字符串,而是设计了 Simple Dynamic String (SDS)数据结构,随着版本演进有不同实现。

SDS 的版本演进

Redis 3.2 之前的 SDS 结构:

c 复制代码
struct sdshdr {
    int len;    // 已使用的字节数量
    int free;   // 剩余可用字节数量
    char buf[]; // 字符数组,实际数据
};

Redis 3.2 及以后版本根据字符串长度使用不同的结构体优化内存使用:

c 复制代码
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;      // 已使用长度(最大255字节)
    uint8_t alloc;    // 分配的总长度
    unsigned char flags; // 类型标志(低3位表示类型)
    char buf[];       // 字符数组
};

// 类似地有sdshdr16/32/64,分别用于不同长度范围的字符串

Redis 7.0 维持了这种结构,但对内存管理进行了优化,并保持了字符串最大长度限制为 512MB。

SDS 与 C 字符串的关键区别

  1. O(1)时间复杂度获取字符串长度:直接访问 len 字段,而 C 字符串需 O(N)遍历
  2. 内存安全:修改前检查空间是否充足,避免缓冲区溢出
  3. 高效内存管理:通过预分配和惰性释放减少内存操作次数
  4. 二进制安全:可存储任意二进制数据,不受'\0'字符限制

"二进制安全"意味着字符串可以存储任何字节序列,包括空字符和非打印字符,而不会被截断或误解。"惰性释放"指字符串缩短时不立即释放多余空间,而是保留用于将来可能的增长。

Redis 对象系统与 String 编码

Redis 使用统一的对象系统(redisObject)管理所有数据:

c 复制代码
typedef struct redisObject {
    unsigned type:4;      // 类型(String, List等)
    unsigned encoding:4;  // 编码方式
    unsigned lru:LRU_BITS; // LRU信息
    int refcount;         // 引用计数
    void *ptr;            // 指向实际数据
} robj;

String 类型的三种编码

String 类型根据内容自动选择最适合的编码:

  1. int 编码:存储可用 64 位有符号整数表示的数值
  2. embstr 编码:存储短字符串(≤44 字节),redisObject 和 SDS 在内存中连续存储
  3. raw 编码:存储长字符串(>44 字节),redisObject 和 SDS 在内存中分开存储

embstr 的 44 字节阈值原理

Redis 5.0 及以上版本中 embstr 的 44 字节阈值计算:

scss 复制代码
44字节 = 64字节(jemalloc分配的最小内存块) - 16字节(redisObject大小) - 3字节(sdshdr8大小) - 1字节('\0')

这样设计确保一个 embstr 字符串恰好占用一个 jemalloc 分配的内存块,优化内存使用效率。Redis 默认使用 jemalloc 而非 libc malloc,因为 jemalloc 能更好地处理内存碎片问题,特别是在长时间运行的服务中。

内存分配与管理策略

SDS 的空间分配采用预分配策略,避免频繁的内存重分配:

从 Redis 4.0 开始,引入了内存碎片整理功能,可以通过activedefrag相关配置启用,有助于减少 String 类型频繁修改造成的内存碎片问题。

编码转换规则与原理

Redis String 类型的编码转换遵循以下规则:

  1. int → embstr/raw:当对整数值执行非数值操作(如 APPEND)时发生
  2. embstr → raw:当修改 embstr 编码字符串时发生,因为 embstr 是只读的
  3. raw → embstr/int:不会发生,编码降级不存在
  4. embstr/raw → int:当使用 SET 等命令将值设为整数时发生

embstr 转换为 raw 的技术原因:embstr 是一整块连续内存,修改可能需要重分配空间,而直接转为 raw 编码(两块分离内存)操作更高效。

Java 实现

连接池配置

java 复制代码
import redis.clients.jedis.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public class RedisConnectionManager implements AutoCloseable {
    private static final Logger logger = LoggerFactory.getLogger(RedisConnectionManager.class);
    private final JedisPool jedisPool;

    public RedisConnectionManager(String host, int port, String password) {
        JedisPoolConfig poolConfig = createPoolConfig();
        if (password != null && !password.isEmpty()) {
            this.jedisPool = new JedisPool(poolConfig, host, port, 2000, password);
        } else {
            this.jedisPool = new JedisPool(poolConfig, host, port, 2000);
        }
        logger.info("Redis connection pool initialized with host: {}, port: {}", host, port);
    }

    private JedisPoolConfig createPoolConfig() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        // 核心连接池配置
        poolConfig.setMaxTotal(100);           // 最大连接数
        poolConfig.setMaxIdle(20);             // 最大空闲连接
        poolConfig.setMinIdle(10);             // 最小空闲连接
        poolConfig.setMaxWaitMillis(3000);     // 获取连接最大等待时间

        // 健康检查配置
        poolConfig.setTestOnBorrow(true);      // 获取连接前测试
        poolConfig.setTestOnReturn(false);     // 归还连接时不测试
        poolConfig.setTestWhileIdle(true);     // 空闲时测试连接
        poolConfig.setTimeBetweenEvictionRunsMillis(60000); // 驱逐线程运行间隔

        // 资源管理配置
        poolConfig.setBlockWhenExhausted(true);  // 连接耗尽时阻塞
        poolConfig.setJmxEnabled(true);          // 启用JMX监控

        return poolConfig;
    }

    public Jedis getConnection() {
        return jedisPool.getResource();
    }

    @Override
    public void close() {
        if (jedisPool != null && !jedisPool.isClosed()) {
            jedisPool.close();
            logger.info("Redis connection pool closed");
        }
    }

    // 使用示例
    public static void main(String[] args) {
        try (RedisConnectionManager connectionManager = new RedisConnectionManager("localhost", 6379, null);
             Jedis jedis = connectionManager.getConnection()) {

            // 验证不同编码
            jedis.set("number", "10086");
            jedis.set("name", "Zhang San");

            // 查看内部编码
            logger.info("number encoding: {}", jedis.objectEncoding("number"));
            logger.info("name encoding: {}", jedis.objectEncoding("name"));

            // 修改验证编码转换
            jedis.append("name", " is a developer");
            logger.info("name encoding after append: {}", jedis.objectEncoding("name"));

        } catch (Exception e) {
            logger.error("Redis operation failed", e);
        }
    }
}

分布式锁实现

java 复制代码
import redis.clients.jedis.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;

public class RedisDistributedLock implements AutoCloseable {
    private static final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
    private final JedisPool jedisPool;
    private final String lockKey;
    private final String lockValue;
    private final int expireTimeSeconds;
    private final LockConfig config;
    private volatile ScheduledExecutorService watchdogExecutor;
    private volatile ScheduledFuture<?> watchdogTask;
    private volatile boolean locked = false;

    public static class LockConfig {
        private final int retryTimes;
        private final long retryIntervalMillis;
        private final int watchdogIntervalSeconds;

        public LockConfig(int retryTimes, long retryIntervalMillis, int watchdogIntervalSeconds) {
            this.retryTimes = retryTimes;
            this.retryIntervalMillis = retryIntervalMillis;
            this.watchdogIntervalSeconds = watchdogIntervalSeconds;
        }

        public static LockConfig DEFAULT = new LockConfig(3, 1000, 10);
    }

    public RedisDistributedLock(JedisPool jedisPool, String lockKey, int expireTimeSeconds) {
        this(jedisPool, lockKey, expireTimeSeconds, LockConfig.DEFAULT);
    }

    public RedisDistributedLock(JedisPool jedisPool, String lockKey,
                               int expireTimeSeconds, LockConfig config) {
        this.jedisPool = jedisPool;
        this.lockKey = "lock:" + lockKey;
        this.lockValue = UUID.randomUUID().toString();
        this.expireTimeSeconds = expireTimeSeconds;
        this.config = config;

        // 创建看门狗线程池
        this.watchdogExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread thread = new Thread(r, "lock-watchdog-" + lockKey);
            thread.setDaemon(true);
            return thread;
        });
    }

    public boolean acquire() {
        int attempted = 0;
        do {
            try (Jedis jedis = jedisPool.getResource()) {
                String result = jedis.set(lockKey, lockValue,
                                        SetParams.setParams().nx().ex(expireTimeSeconds));
                if ("OK".equals(result)) {
                    locked = true;
                    logger.debug("Lock {} acquired by thread {}",
                                lockKey, Thread.currentThread().getName());
                    startWatchdog();
                    return true;
                }

                attempted++;
                if (attempted < config.retryTimes) {
                    logger.debug("Failed to acquire lock: {}, retry {}/{}",
                                lockKey, attempted, config.retryTimes);
                    Thread.sleep(config.retryIntervalMillis);
                }
            } catch (JedisConnectionException e) {
                logger.warn("Redis connection failed when acquiring lock: {}", lockKey, e);
                attempted++;
                if (attempted >= config.retryTimes) {
                    break;
                }
                try {
                    Thread.sleep(config.retryIntervalMillis);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        } while (attempted < config.retryTimes);

        logger.warn("Failed to acquire lock after {} retries: {}",
                   config.retryTimes, lockKey);
        return false;
    }

    public boolean release() {
        if (!locked) {
            return false;
        }

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('del', KEYS[1]) else return 0 end";
        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(script,
                                     Collections.singletonList(lockKey),
                                     Collections.singletonList(lockValue));
            boolean success = Long.valueOf(1L).equals(result);
            if (success) {
                logger.debug("Lock {} released by thread {}",
                            lockKey, Thread.currentThread().getName());
                locked = false;
                stopWatchdog();
            } else {
                logger.warn("Failed to release lock: {}, it might have expired", lockKey);
            }
            return success;
        } catch (JedisException e) {
            logger.error("Error when releasing lock: {}", lockKey, e);
            return false;
        }
    }

    private void startWatchdog() {
        stopWatchdog(); // 确保不会重复启动

        // 计算续期间隔,取过期时间的1/3,最小1秒
        int renewInterval = Math.max(1, expireTimeSeconds / 3);

        watchdogTask = watchdogExecutor.scheduleAtFixedRate(() -> {
            try (Jedis jedis = jedisPool.getResource()) {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                               "return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
                Object result = jedis.eval(script,
                                         Collections.singletonList(lockKey),
                                         Arrays.asList(lockValue, String.valueOf(expireTimeSeconds)));
                if (Long.valueOf(1L).equals(result)) {
                    logger.debug("Lock {} renewed for {} seconds", lockKey, expireTimeSeconds);
                } else {
                    logger.warn("Failed to renew lock: {}, it might have been acquired by another process", lockKey);
                    stopWatchdog();
                }
            } catch (Exception e) {
                logger.error("Error when renewing lock: {}", lockKey, e);
            }
        }, renewInterval, renewInterval, TimeUnit.SECONDS);
    }

    private void stopWatchdog() {
        if (watchdogTask != null && !watchdogTask.isCancelled()) {
            watchdogTask.cancel(false);
            watchdogTask = null;
        }
    }

    @Override
    public void close() {
        release();
        if (watchdogExecutor != null) {
            watchdogExecutor.shutdownNow();
            watchdogExecutor = null;
        }
    }

    // 扩展功能:执行受锁保护的代码块
    public <T> T executeWithLock(Supplier<T> task) {
        if (!acquire()) {
            throw new RuntimeException("Failed to acquire lock: " + lockKey);
        }

        try {
            return task.get();
        } finally {
            release();
        }
    }

    // 使用示例
    public static void main(String[] args) {
        JedisPool jedisPool = new JedisPool("localhost", 6379);
        String resourceKey = "inventory:item:10001";

        try (RedisDistributedLock lock = new RedisDistributedLock(jedisPool, resourceKey, 30)) {
            // 函数式方式使用锁
            Integer updatedCount = lock.executeWithLock(() -> {
                // 这里是需要锁保护的原子操作
                try (Jedis jedis = jedisPool.getResource()) {
                    String countStr = jedis.get(resourceKey);
                    int count = countStr == null ? 0 : Integer.parseInt(countStr);
                    count -= 1; // 减库存
                    jedis.set(resourceKey, String.valueOf(count));
                    return count;
                }
            });

            System.out.println("Updated inventory count: " + updatedCount);
        } catch (Exception e) {
            System.err.println("Operation failed: " + e.getMessage());
        } finally {
            jedisPool.close();
        }
    }
}

Repository 模式与断路器模式结合

java 复制代码
import redis.clients.jedis.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.function.Supplier;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;

public class RedisRepository<T> {
    private static final Logger logger = LoggerFactory.getLogger(RedisRepository.class);
    private final JedisPool jedisPool;
    private final ObjectMapper objectMapper;
    private final Class<T> entityClass;
    private final String keyPrefix;
    private final int defaultExpireSeconds;
    private final CircuitBreaker circuitBreaker;

    public RedisRepository(JedisPool jedisPool, Class<T> entityClass, String keyPrefix, int defaultExpireSeconds) {
        this.jedisPool = jedisPool;
        this.objectMapper = new ObjectMapper();
        this.entityClass = entityClass;
        this.keyPrefix = keyPrefix;
        this.defaultExpireSeconds = defaultExpireSeconds;

        // 配置断路器
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)                 // 50%的失败率触发断路
            .waitDurationInOpenState(Duration.ofSeconds(10)) // 断路器打开10秒后尝试半开状态
            .ringBufferSizeInHalfOpenState(5)         // 半开状态下尝试5次请求
            .ringBufferSizeInClosedState(10)          // 关闭状态下记录10次请求结果
            .build();

        CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
        this.circuitBreaker = registry.circuitBreaker(keyPrefix + "-circuit-breaker");

        logger.info("Redis repository initialized for entity: {}, prefix: {}",
                   entityClass.getSimpleName(), keyPrefix);
    }

    // 保存实体
    public void save(String id, T entity) {
        save(id, entity, defaultExpireSeconds);
    }

    public void save(String id, T entity, int expireSeconds) {
        String key = buildKey(id);
        Supplier<Void> saveOperation = () -> {
            try (Jedis jedis = jedisPool.getResource()) {
                String json = objectMapper.writeValueAsString(entity);
                Pipeline pipeline = jedis.pipelined();
                pipeline.set(key, json);
                pipeline.expire(key, expireSeconds);
                pipeline.sync();
                logger.debug("Entity {} with id {} saved with expiration {} seconds",
                            entityClass.getSimpleName(), id, expireSeconds);
                return null;
            } catch (Exception e) {
                logger.error("Failed to save entity {} with id {}",
                            entityClass.getSimpleName(), id, e);
                throw new RedisOperationException("Save operation failed", e);
            }
        };

        try {
            circuitBreaker.executeSupplier(saveOperation);
        } catch (Exception e) {
            logger.error("Circuit breaker prevented save operation for {} with id {}",
                        entityClass.getSimpleName(), id, e);
            throw new CircuitBreakerException("Save operation prevented by circuit breaker", e);
        }
    }

    // 获取实体
    public T findById(String id) {
        String key = buildKey(id);
        Supplier<T> findOperation = () -> {
            try (Jedis jedis = jedisPool.getResource()) {
                String json = jedis.get(key);
                if (json == null) {
                    logger.debug("Entity {} with id {} not found",
                                entityClass.getSimpleName(), id);
                    return null;
                }
                T entity = objectMapper.readValue(json, entityClass);
                logger.debug("Entity {} with id {} retrieved",
                            entityClass.getSimpleName(), id);
                return entity;
            } catch (Exception e) {
                logger.error("Failed to retrieve entity {} with id {}",
                            entityClass.getSimpleName(), id, e);
                throw new RedisOperationException("Find operation failed", e);
            }
        };

        try {
            return circuitBreaker.executeSupplier(findOperation);
        } catch (Exception e) {
            logger.error("Circuit breaker prevented find operation for {} with id {}",
                        entityClass.getSimpleName(), id, e);
            throw new CircuitBreakerException("Find operation prevented by circuit breaker", e);
        }
    }

    // 删除实体
    public boolean delete(String id) {
        String key = buildKey(id);
        Supplier<Boolean> deleteOperation = () -> {
            try (Jedis jedis = jedisPool.getResource()) {
                Long result = jedis.del(key);
                boolean deleted = result != null && result > 0;
                if (deleted) {
                    logger.debug("Entity {} with id {} deleted",
                                entityClass.getSimpleName(), id);
                } else {
                    logger.debug("Entity {} with id {} not found for deletion",
                                entityClass.getSimpleName(), id);
                }
                return deleted;
            } catch (Exception e) {
                logger.error("Failed to delete entity {} with id {}",
                            entityClass.getSimpleName(), id, e);
                throw new RedisOperationException("Delete operation failed", e);
            }
        };

        try {
            return circuitBreaker.executeSupplier(deleteOperation);
        } catch (Exception e) {
            logger.error("Circuit breaker prevented delete operation for {} with id {}",
                        entityClass.getSimpleName(), id, e);
            throw new CircuitBreakerException("Delete operation prevented by circuit breaker", e);
        }
    }

    // 增量计数器
    public long increment(String id, String field, long amount) {
        String key = buildKey(id + ":" + field);
        Supplier<Long> incrementOperation = () -> {
            try (Jedis jedis = jedisPool.getResource()) {
                long result;
                if (amount == 1) {
                    result = jedis.incr(key);
                } else {
                    result = jedis.incrBy(key, amount);
                }

                // 如果是新创建的计数器,设置过期时间
                if (result == amount) {
                    jedis.expire(key, defaultExpireSeconds);
                }

                logger.debug("Counter {} for entity {} with id {} incremented by {}, new value: {}",
                            field, entityClass.getSimpleName(), id, amount, result);
                return result;
            } catch (Exception e) {
                logger.error("Failed to increment counter {} for entity {} with id {}",
                            field, entityClass.getSimpleName(), id, e);
                throw new RedisOperationException("Increment operation failed", e);
            }
        };

        try {
            return circuitBreaker.executeSupplier(incrementOperation);
        } catch (Exception e) {
            logger.error("Circuit breaker prevented increment operation for {} with id {}",
                        entityClass.getSimpleName(), id, e);
            throw new CircuitBreakerException("Increment operation prevented by circuit breaker", e);
        }
    }

    private String buildKey(String id) {
        return keyPrefix + ":" + id;
    }

    // 自定义异常
    public static class RedisOperationException extends RuntimeException {
        public RedisOperationException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    public static class CircuitBreakerException extends RuntimeException {
        public CircuitBreakerException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

// 使用示例
class Product {
    private String id;
    private String name;
    private double price;

    // getters and setters
}

class ProductService {
    private final RedisRepository<Product> productRepository;

    public ProductService(JedisPool jedisPool) {
        this.productRepository = new RedisRepository<>(jedisPool, Product.class, "product", 3600);
    }

    public void saveProduct(Product product) {
        productRepository.save(product.getId(), product);
    }

    public Product getProduct(String id) {
        return productRepository.findById(id);
    }

    public long incrementViews(String productId) {
        return productRepository.increment(productId, "views", 1);
    }
}

Bitmap 应用示例

java 复制代码
import redis.clients.jedis.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;

public class UserActivityTracker {
    private static final Logger logger = LoggerFactory.getLogger(UserActivityTracker.class);
    private final JedisPool jedisPool;
    private final String keyPrefix;

    public UserActivityTracker(JedisPool jedisPool, String keyPrefix) {
        this.jedisPool = jedisPool;
        this.keyPrefix = keyPrefix;
    }

    // 记录用户当日签到
    public void recordSignIn(long userId) {
        String key = getSignInKey(userId);
        int dayOfMonth = LocalDate.now().getDayOfMonth();

        try (Jedis jedis = jedisPool.getResource()) {
            Boolean wasAlreadySet = jedis.getbit(key, dayOfMonth - 1);
            jedis.setbit(key, dayOfMonth - 1, true);

            // 如果是新创建的键,设置过期时间(当月结束后再保留7天)
            if (Boolean.FALSE.equals(wasAlreadySet)) {
                LocalDate firstDayOfNextMonth = LocalDate.now().plusMonths(1).withDayOfMonth(1);
                LocalDate expiryDate = firstDayOfNextMonth.plusDays(7);
                int ttlSeconds = (int) (expiryDate.toEpochDay() - LocalDate.now().toEpochDay()) * 86400;
                jedis.expire(key, ttlSeconds);

                logger.info("User {} sign-in recorded for day {}, first time this month",
                           userId, dayOfMonth);
            } else {
                logger.info("User {} already signed in for day {}", userId, dayOfMonth);
            }
        } catch (JedisException e) {
            logger.error("Failed to record sign-in for user {}", userId, e);
            throw new RuntimeException("Failed to record sign-in", e);
        }
    }

    // 检查用户是否已签到
    public boolean hasSignedIn(long userId) {
        String key = getSignInKey(userId);
        int dayOfMonth = LocalDate.now().getDayOfMonth();

        try (Jedis jedis = jedisPool.getResource()) {
            Boolean result = jedis.getbit(key, dayOfMonth - 1);
            return Boolean.TRUE.equals(result);
        } catch (JedisException e) {
            logger.error("Failed to check sign-in status for user {}", userId, e);
            throw new RuntimeException("Failed to check sign-in status", e);
        }
    }

    // 获取用户本月签到次数
    public long getMonthlySignInCount(long userId) {
        String key = getSignInKey(userId);

        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.bitcount(key);
        } catch (JedisException e) {
            logger.error("Failed to get monthly sign-in count for user {}", userId, e);
            throw new RuntimeException("Failed to get monthly sign-in count", e);
        }
    }

    // 获取用户当月签到情况
    public List<Integer> getMonthlySignInDays(long userId) {
        String key = getSignInKey(userId);
        int daysInMonth = LocalDate.now().lengthOfMonth();
        List<Integer> signInDays = new ArrayList<>();

        try (Jedis jedis = jedisPool.getResource()) {
            // 获取整个位图并在Java中处理,减少Redis往返
            byte[] bytes = jedis.get(key.getBytes());
            if (bytes != null && bytes.length > 0) {
                BitSet bitSet = BitSet.valueOf(bytes);
                for (int i = 0; i < daysInMonth; i++) {
                    if (bitSet.get(i)) {
                        signInDays.add(i + 1); // 添加签到的日期(从1开始)
                    }
                }
            }
            return signInDays;
        } catch (JedisException e) {
            logger.error("Failed to get monthly sign-in days for user {}", userId, e);
            throw new RuntimeException("Failed to get monthly sign-in days", e);
        }
    }

    // 检查是否连续签到
    public int getConsecutiveSignInDays(long userId) {
        List<Integer> days = getMonthlySignInDays(userId);
        if (days.isEmpty()) {
            return 0;
        }

        // 检查今天是否签到
        if (!days.contains(LocalDate.now().getDayOfMonth())) {
            return 0;
        }

        int consecutive = 1;
        int today = LocalDate.now().getDayOfMonth();

        // 从今天向前检查连续签到
        for (int i = 1; i < today; i++) {
            if (!days.contains(today - i)) {
                break;
            }
            consecutive++;
        }

        return consecutive;
    }

    // 跨多个用户的批量操作 - 获取当日活跃用户数
    public long getDailyActiveUserCount(List<Long> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return 0;
        }

        String tempKey = "temp:activeusers:" + System.currentTimeMillis();
        int dayOfMonth = LocalDate.now().getDayOfMonth();

        try (Jedis jedis = jedisPool.getResource()) {
            // 创建临时位图
            for (Long userId : userIds) {
                String key = getSignInKey(userId);
                Boolean isActive = jedis.getbit(key, dayOfMonth - 1);
                if (Boolean.TRUE.equals(isActive)) {
                    jedis.setbit(tempKey, userId, true);
                }
            }

            // 计算临时位图中设置的位数
            long count = jedis.bitcount(tempKey);

            // 删除临时键
            jedis.del(tempKey);

            return count;
        } catch (JedisException e) {
            logger.error("Failed to get daily active user count", e);
            throw new RuntimeException("Failed to get daily active user count", e);
        }
    }

    private String getSignInKey(long userId) {
        // 分片策略 - 对大规模用户进行分片,避免单个位图过大
        int shardIndex = (int)(userId % 10); // 10个分片
        String yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
        return keyPrefix + ":signin:" + shardIndex + ":" + yearMonth + ":" + userId;
    }

    // 使用示例
    public static void main(String[] args) {
        JedisPool jedisPool = new JedisPool("localhost", 6379);
        UserActivityTracker tracker = new UserActivityTracker(jedisPool, "app");

        long userId = 1001;

        // 记录签到
        tracker.recordSignIn(userId);

        // 检查签到状态
        boolean hasSignedIn = tracker.hasSignedIn(userId);
        System.out.println("User has signed in today: " + hasSignedIn);

        // 获取本月签到次数
        long monthlyCount = tracker.getMonthlySignInCount(userId);
        System.out.println("Monthly sign-in count: " + monthlyCount);

        // 获取本月签到日期
        List<Integer> signInDays = tracker.getMonthlySignInDays(userId);
        System.out.println("Sign-in days this month: " + signInDays);

        // 获取连续签到天数
        int consecutiveDays = tracker.getConsecutiveSignInDays(userId);
        System.out.println("Consecutive sign-in days: " + consecutiveDays);

        jedisPool.close();
    }
}

JMH 基准测试示例

java 复制代码
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import redis.clients.jedis.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(value = 1, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class RedisStringBenchmark {
    private JedisPool jedisPool;
    private static final String INT_KEY = "benchmark:int";
    private static final String EMBSTR_KEY = "benchmark:embstr";
    private static final String RAW_KEY = "benchmark:raw";
    private static final String PIPELINED_KEY_PREFIX = "benchmark:pipeline:";

    @Setup
    public void setup() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(32);
        poolConfig.setMaxIdle(16);
        jedisPool = new JedisPool(poolConfig, "localhost", 6379);

        // 准备基准测试数据
        try (Jedis jedis = jedisPool.getResource()) {
            // 用于int编码的数据
            jedis.set(INT_KEY, "12345");

            // 用于embstr编码的短字符串
            jedis.set(EMBSTR_KEY, "This is a short string for embstr encoding test");

            // 用于raw编码的长字符串
            StringBuilder longString = new StringBuilder(5000);
            for (int i = 0; i < 100; i++) {
                longString.append("This is a long string for raw encoding benchmark test. ");
            }
            jedis.set(RAW_KEY, longString.toString());
        }
    }

    @TearDown
    public void tearDown() {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.del(INT_KEY, EMBSTR_KEY, RAW_KEY);
            // 清理pipeline测试键
            for (int i = 0; i < 100; i++) {
                jedis.del(PIPELINED_KEY_PREFIX + i);
            }
        }

        if (jedisPool != null) {
            jedisPool.close();
        }
    }

    @Benchmark
    public String getIntEncoded() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.get(INT_KEY);
        }
    }

    @Benchmark
    public String getEmbstrEncoded() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.get(EMBSTR_KEY);
        }
    }

    @Benchmark
    public String getRawEncoded() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.get(RAW_KEY);
        }
    }

    @Benchmark
    public String setIntValue() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.set(INT_KEY, "54321");
        }
    }

    @Benchmark
    public String setEmbstrValue() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.set(EMBSTR_KEY, "Updated short string for embstr test");
        }
    }

    @Benchmark
    public String setRawValue() {
        try (Jedis jedis = jedisPool.getResource()) {
            StringBuilder updatedString = new StringBuilder(5000);
            for (int i = 0; i < 50; i++) {
                updatedString.append("Updated long string for raw benchmark test. ");
            }
            return jedis.set(RAW_KEY, updatedString.toString());
        }
    }

    @Benchmark
    public Long incrIntValue() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.incr(INT_KEY);
        }
    }

    @Benchmark
    public String appendToEmbstr() {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.append(EMBSTR_KEY, " append test");
        }
    }

    @Benchmark
    public void pipelinedSetOperations() {
        try (Jedis jedis = jedisPool.getResource()) {
            Pipeline pipeline = jedis.pipelined();
            for (int i = 0; i < 100; i++) {
                pipeline.set(PIPELINED_KEY_PREFIX + i, "value-" + i);
            }
            pipeline.sync();
        }
    }

    @Benchmark
    public List<Object> pipelinedGetOperations() {
        try (Jedis jedis = jedisPool.getResource()) {
            Pipeline pipeline = jedis.pipelined();
            for (int i = 0; i < 100; i++) {
                pipeline.get(PIPELINED_KEY_PREFIX + i);
            }
            return pipeline.syncAndReturnAll();
        }
    }

    // 比较LUA脚本与直接操作
    @Benchmark
    public Long incrWithExpireIndividual() {
        try (Jedis jedis = jedisPool.getResource()) {
            Long value = jedis.incr(INT_KEY);
            jedis.expire(INT_KEY, 3600);
            return value;
        }
    }

    @Benchmark
    public Object incrWithExpireLua() {
        String script = "local value = redis.call('incr', KEYS[1]) " +
                       "redis.call('expire', KEYS[1], ARGV[1]) " +
                       "return value";
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.eval(script,
                            Collections.singletonList(INT_KEY),
                            Collections.singletonList("3600"));
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(RedisStringBenchmark.class.getSimpleName())
            .build();

        new Runner(opt).run();
    }
}

Redis String 使用决策

优化建议

  1. 编码选择

    • 计数器场景优先使用 int 编码
    • 短文本(≤44 字节)使用 embstr 编码
    • 避免频繁修改 embstr 编码的字符串
  2. 操作优化

    • 使用 Pipeline 减少网络往返
    • 使用原子操作代替 GET+SET 模式
    • 批量操作使用 MGET/MSET
    • 使用 LUA 脚本进行复杂的原子操作
    • 位操作场景使用 GETBIT/SETBIT
  3. 内存管理

    • 设置合理的 maxmemory 和淘汰策略
    • 启用 activedefrag 减少内存碎片
    • 合理使用过期时间
    • 压缩键名和值减少内存占用
  4. 高可用与可扩展性

    • 主从复制确保数据可用性
    • Sentinel 提供自动故障转移
    • 集群模式分散数据,提高可扩展性
    • 使用哈希标签处理集群环境中的关联数据
    • RedLock 算法解决跨节点分布式锁问题
  5. 连接管理

    • 使用连接池管理连接资源
    • 设置合理的连接超时和重试策略
    • 实现断路器模式处理服务不可用情况
    • 监控并优化连接池配置参数

总结

特性 实现方式 优势 注意事项
数据结构 SDS O(1)获取长度, 内存安全, 二进制安全 根据长度选择不同结构体
编码方式 int/embstr/raw 自动选择最佳编码,优化性能和内存 embstr 修改会转为 raw
内存管理 预分配+惰性释放 减少内存操作,提高性能 可能导致暂时内存浪费
原子操作 命令集+事务+LUA 脚本 支持复杂的原子操作,减少竞态条件 事务不支持回滚
高可用 主从+哨兵+集群 提高可用性和可扩展性 需注意主从一致性延迟
应用场景 缓存/计数器/分布式锁/位图应用 丰富的应用场景支持 选择合适的数据结构和编码
监控运维 Prometheus/Grafana 全面监控性能和健康状况 关注内存使用和命令统计
相关推荐
JH30733 分钟前
Java Stream API 在企业开发中的实战心得:高效、优雅的数据处理
java·开发语言·oracle
九月十九2 小时前
java使用aspose读取word里的图片
java·word
一 乐4 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
爱记录的小磊4 小时前
java-selenium自动化快速入门
java·selenium·自动化
鹏码纵横4 小时前
已解决:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver 异常的正确解决方法,亲测有效!!!
java·python·mysql
weixin_985432114 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
Mr Aokey4 小时前
Java UDP套接字编程:高效实时通信的实战应用与核心类解析
java·java-ee
冬天vs不冷4 小时前
Java分层开发必知:PO、BO、DTO、VO、POJO概念详解
java·开发语言
hong_zc4 小时前
Java 文件操作与IO流
java·文件操作·io 流