授课语音

Redis 基本知识

1. 介绍

Redis 是用 C 语言开发的开源高性能键值数据库,广泛应用于缓存、消息队列等场景。2023 年 Redis 7.0 版本发布,具有以下特点:

  • 高性能:在读写性能方面表现卓越,适用于高并发场景。
  • 持久化:支持 RDB(快照备份)和 AOF(文件追加)两种持久化方式,确保数据安全。
  • 丰富的数据类型:支持字符串、列表、哈希、集合、有序集合等多种数据类型。
  • 发布/订阅:支持消息的发布和订阅,适用于简单的实时消息系统。
  • 事务:通过事务和 Lua 脚本,确保多个命令的原子执行。支持简单的事务,确保数据一致性。
  • 分布式:支持主从复制、分片和高可用架构(如 Redis Sentinel、Redis Cluster)。

Redis 支持多种数据结构和操作,具体包括:

  • 字符串(String:存储简单的键值对(K/V),值可以是字符串、整数或二进制数据,用于计数(如点赞数)、缓存对象(序列化的对象)、分布式锁(设置带有过期时间的键)等。
  • 列表(List:按插入顺序的字符串集合,支持双端操作,可用作消息队列、任务队列、存储文章评论(按时间顺序)等。
  • 集合(Set:不允许重复元素的集合,支持并集、交集和差集操作,适用于抽奖活动(参与一次抽奖的用户)、标签系统(文章或用户标签)等。
  • 有序集合(Sorted Set:有序的集合,每个元素都有一个分数,根据分数排序,可用在游戏积分排行榜、范围查询(商品价格排序)等。
  • 哈希(Hash:存储键值对的集合,可用作配置管理(存储应用程序的配置信息)等。
  • 位图(Bitmap:字符串类型的扩展,用于存储连续的二进制数字,极大节省空间,可用在用户签到(记录用户每天签到情况)、状态标记(用户是否在线)、统计数据(统计某个时间段内的活跃用户)等。
  • 基数统计(HyperLoglog:占用空间非常小(12KB 可存储接近 2^64 个不同元素),计数结果存在一定的误差,可用于基数统计(统计网站的独立访问用户数)。
  • 地理位置(GEO:有序集合的扩展,存储地理位置信息,可用在位置服务(搜索附近餐馆、商店等)。
  • 流(Stream:支持按时间排序的消息流,可用于日志系统(存储和处理日志数据)、消息队列。

2. 代码案例

下面的代码展示了 Redis 的各种应用场景实践,使用 Java 进行实现。

2.1 Maven 依赖

在项目的 pom.xml 中添加 Jedis 依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.4.6</version>
</dependency>

2.2 Java 示例代码

package com.zhilitech;

import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.args.GeoUnit;
import redis.clients.jedis.params.GeoRadiusParam;
import redis.clients.jedis.params.XAddParams;
import redis.clients.jedis.params.XTrimParams;
import redis.clients.jedis.resps.GeoRadiusResponse;
import redis.clients.jedis.resps.StreamEntry;
import redis.clients.jedis.resps.Tuple;

import java.util.*;

public class RedisExamples {
    // 创建 Jedis 连接池
    private static JedisPool jedisPool;

    // 配置和初始化 Jedis 连接池
    private static void setupJedis() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(10); // 设置最大连接数
        poolConfig.setMinIdle(2);   // 设置最小空闲连接数
        poolConfig.setMaxIdle(5);   // 设置最大空闲连接数
        poolConfig.setTestOnBorrow(true); // 在借用连接前进行验证

        String redisHost = "localhost"; // Redis 服务器地址
        int redisPort = 6379;            // Redis 服务器端口
        String redisPassword = null;     // Redis 密码(如有)
        int redisDb = 3;                 // Redis 数据库索引
        jedisPool = new JedisPool(poolConfig, redisHost, redisPort, 2000, redisPassword, redisDb);
    }

    // 检查用户是否连续签到 N 天
    private static boolean checkConsecutiveDays(Jedis jedis, String bitmapKey, int days) {
        // 获取位图中签到的总天数
        long bitmapLength = jedis.bitcount(bitmapKey);

        // 如果签到天数小于要求的天数,则返回 false
        if (bitmapLength < days) {
            return false;
        }
        // 遍历每个可能的连续签到窗口
        for (int i = 0; i <= bitmapLength - days; i++) {
            long count = jedis.bitcount(bitmapKey, i, i + days - 1);
            // 如果该窗口内的所有位都为 1,则表示连续签到
            if (count == days) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        setupJedis();
        // 创建 Jedis 客户端
        try (Jedis jedis = jedisPool.getResource()) {

            // **字符串(String)操作示例**
            // 设置和获取简单的键值对
            jedis.set("user:1000:likes", "50");
            String likes = jedis.get("user:1000:likes");
            System.out.println("User 1000 likes: " + likes); // 输出: User 1000 likes: 50
            
            // 设置带有过期时间的键(用于分布式锁)
            jedis.setex("lock:resource", 60, "locked");
            String lockStatus = jedis.get("lock:resource");
            System.out.println("Resource lock status: " + lockStatus); // 输出: Resource lock status: locked
            
            // 增加整数值
            jedis.set("page_views", "0"); // 初始化页面浏览量
            jedis.incr("page_views");      // 增加 1
            jedis.incrBy("page_views", 10); // 增加 10
            System.out.println("Page views count: " + jedis.get("page_views")); // 输出: Page views count: 11

            // **列表(List)操作示例**
            // 使用列表作为任务队列
            jedis.del("task_queue"); // 清空任务队列
            jedis.rpush("task_queue", "task1"); // 将任务加入队列
            jedis.rpush("task_queue", "task2");
            String task = jedis.lpop("task_queue"); // 从队列中取出任务
            System.out.println("Processed task: " + task); // 输出: Processed task: task1

            // 存储文章评论
            jedis.del("comments:article:123"); // 清空评论
            jedis.rpush("comments:article:123", "Great article!"); // 添加评论
            jedis.rpush("comments:article:123", "Very informative.");
            List<String> comments = jedis.lrange("comments:article:123", 0, -1); // 获取所有评论
            System.out.println("Article comments: " + comments); // 输出: Article comments: [Great article!, Very informative.]

            // **集合(Set)操作示例**
            // 管理用户标签
            jedis.sadd("tags:user:1000", "python"); // 添加标签
            jedis.sadd("tags:user:1000", "redis");
            Set<String> tags = jedis.smembers("tags:user:1000"); // 获取用户标签
            System.out.println("User 1000 tags: " + tags); // 输出: User 1000 tags: [python, redis]

            // 执行集合操作:并集、交集、差集
            jedis.sadd("setA", "apple", "banana", "cherry"); // 创建集合 A
            jedis.sadd("setB", "banana", "cherry", "date");  // 创建集合 B
            Set<String> union = jedis.sunion("setA", "setB"); // 获取并集
            Set<String> intersection = jedis.sinter("setA", "setB"); // 获取交集
            Set<String> difference = jedis.sdiff("setA", "setB"); // 获取差集
            System.out.println("Union: " + union); // 输出: Union: [apple, banana, cherry, date]
            System.out.println("Intersection: " + intersection); // 输出: Intersection: [banana, cherry]
            System.out.println("Difference: " + difference); // 输出: Difference: [apple]

            // **有序集合(Sorted Set)

操作示例**
            // 存储游戏积分排行榜
            jedis.zadd("leaderboard", 100, "player1"); // 添加玩家及其分数
            jedis.zadd("leaderboard", 150, "player2");
            jedis.zadd("leaderboard", 120, "player3");
            // 获取排行榜前两名
            Set<Tuple> topPlayers = new HashSet<>(jedis.zrevrangeWithScores("leaderboard", 0, 1));
            System.out.println("Top players: " + topPlayers); // 输出: Top players: [player2:150.0, player3:120.0]
            // 根据分数范围查询
            Set<String> playersInRange = new HashSet<>(jedis.zrangeByScore("leaderboard", 100, 130));
            System.out.println("Players with scores between 100 and 130: " + playersInRange); // 输出: Players with scores between 100 and 130: [player1, player3]

            // **哈希(Hash)操作示例**
            // 存储用户配置
            jedis.hset("user:1000:config", "theme", "dark"); // 设置用户配置
            jedis.hset("user:1000:config", "notifications", "enabled");
            Map<String, String> config = jedis.hgetAll("user:1000:config"); // 获取用户配置
            System.out.println("User 1000 config: " + config); // 输出: User 1000 config: {theme=dark, notifications=enabled}

            // **位图(Bitmap)操作示例**
            // 记录用户签到
            jedis.setbit("user:1000:sign_in", 0, true); // 第一天签到
            jedis.setbit("user:1000:sign_in", 1, false); // 第二天未签到
            // 统计签到情况
            long signInDays = jedis.bitcount("user:1000:sign_in");
            System.out.println("User 1000 sign-in days: " + signInDays); // 输出: User 1000 sign-in days: 1
            
            // 位图键
            String userId = "user:1000";
            String bitmapKey = userId + ":sign_in";
            // 模拟签到数据
            // 假设第 0 位表示第 1 天签到,第 1 位表示第 2 天签到,以此类推
            for (int i = 0; i < 7; i++) {
                jedis.setbit(bitmapKey, i, true); // 设置用户第 1 天到第 7 天都已签到
            }
            // 检查用户是否连续签到 7 天
            boolean isConsecutive = checkConsecutiveDays(jedis, bitmapKey, 7);
            System.out.println("User has signed in for 7 consecutive days: " + isConsecutive); // 输出: User has signed in for 7 consecutive days: true

            // **基数统计(HyperLoglog)操作示例**
            // 统计唯一访问用户数
            jedis.pfadd("unique_users", "user1", "user2", "user3", "user1"); // user1 访问了多次,但只算一次
            long uniqueUsersCount = jedis.pfcount("unique_users"); // 计算唯一用户数量
            System.out.println("Estimated unique users: " + uniqueUsersCount); // 输出: Estimated unique users: 3

            // **地理位置(GEO)操作示例**
            // 存储地理位置
            jedis.geoadd("places", 13.361389, 38.115556, "Palermo"); // 帕勒莫,意大利
            jedis.geoadd("places", 15.087269, 37.502669, "Catania"); // 卡塔尼亚,意大利
            jedis.geoadd("places", 2.173404, 41.385064, "Barcelona"); // 巴塞罗那,西班牙
            jedis.geoadd("places", 10.451526, 51.165691, "Germany");   // 德国,中心位置
            jedis.geoadd("places", -3.703790, 40.416775, "Madrid");    // 马德里,西班牙

            // 查询附近地点(半径 100km)
            List<GeoRadiusResponse> nearbyPlaces = jedis.georadius("places", 15.087269, 37.502669, 100, GeoUnit.KM);
            System.out.println("Nearby places within 100 km:");
            for (GeoRadiusResponse item : nearbyPlaces) {
                String name = item.getMemberByString(); // 获取地点名称
                GeoCoordinate coord = item.getCoordinate(); // 获取坐标
                System.out.println("Place: " + name + (coord != null ? ", Location: " + coord.getLongitude() + ", " + coord.getLatitude() : ""));
                // 预期输出:
                // Place: Catania, Location: 15.087269, 37.502669
                // Place: Palermo, Location: 13.361389, 38.115556
            }

            // 获取指定地点的地理坐标
            List<GeoCoordinate> positions = jedis.geopos("places", "Palermo", "Catania", "Barcelona", "Germany", "Madrid");
            System.out.println("Geographic positions:");
            for (GeoCoordinate position : positions) {
                String place = position == null ? "Unknown" : position.toString();
                System.out.println("Place: " + place);
            }

            // 查找特定半径内的地点及其距离
            List<GeoRadiusResponse> placesWithDistance = jedis.georadius("places", 15.087269, 37.502669, 1000, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist());
            System.out.println("Nearby places with distance within 1000 km:");
            for (GeoRadiusResponse item : placesWithDistance) {
                String name = item.getMemberByString(); // 获取地点名称
                double distance = item.getDistance(); // 获取距离,单位为公里
                System.out.println("Place: " + name + ", Distance: " + distance + " km");
            }

            // **流(Stream)操作示例**
            // 记录日志消息
            Map<String, String> logEntry1 = new HashMap<>();
            logEntry1.put("message", "System started");
            logEntry1.put("level", "info");
            jedis.xadd("logs", logEntry1, new XAddParams());

            Map<String, String> logEntry2 = new HashMap<>();
            logEntry2.put("message", "User login failed");
            logEntry2.put("level", "error");
            jedis.xadd("logs", logEntry2, new XAddParams());

            // 读取日志消息
            List<StreamEntry> messages = jedis.xrange("logs", "0", "+", 2);
            System.out.println("Log messages: " + messages);

            // 限制流长度(保留最新的 1000 条消息)
            jedis.xtrim("logs", new XTrimParams().maxLen(1000));
            System.out.println("Stream length after trimming: " + jedis.xlen("logs")); // 输出: Stream length after trimming: 4 
        }
    }
}

代码说明

  1. 连接池配置:使用 JedisPool 来管理 Redis 连接,设置最大连接数、最小空闲连接数等。
  2. 字符串操作:展示了设置和获取键值对、使用过期时间的分布式锁以及整数增量操作。
  3. 列表操作:演示了如何使用 Redis 列表作为任务队列和存储文章评论。
  4. 集合操作:示范了标签管理及集合的并集、交集和差集操作。
  5. 有序集合操作:展示了如何存储游戏积分排行榜以及根据分数范围查询。
  6. 哈希操作:演示了如何存储和获取用户配置。
  7. 位图操作:记录用户签到情况,并检查是否连续签到。
  8. 基数统计:使用 HyperLoglog 统计唯一用户数。
  9. 地理位置操作:存储地点信息并查询附近地点及其距离。
  10. 流操作:记录日志信息,读取日志流,并限制流长度。

此代码案例涵盖了 Redis 的基本操作和多种应用场景,为开发者提供了良好的参考。根据实际需要,可以在此基础上扩展功能和细节。

去1:1私密咨询

系列课程: