【技术派后端篇】技术派中基于 Redis 的缓存实践

在互联网应用追求高并发和高可用的背景下,缓存对于提升程序性能至关重要。相较于本地缓存 Guava Cache 和 Caffeine,Redis 具有显著优势。Redis 支持集群和分布式部署,能横向扩展缓存容量和负载能力,适应大型分布式系统的缓存需求;支持数据持久化存储,可将缓存数据存于磁盘,保障数据不丢失;支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等,提供更灵活的缓存能力;具备主从同步和哨兵机制,实现高可用性和容错能力;还提供丰富的数据处理命令,如排序、聚合、管道和 Lua 脚本等,便于高效处理缓存数据。

Redis 本质上是一个开源的基于内存的 NoSQL 数据库,更适合作为数据库前的缓存层组件。它支持多种数据结构,如 String、List、Set、Hash、ZSet 等,并且支持数据持久化,通过快照和日志将内存数据保存到硬盘,重启后可再次加载使用,其主从复制、哨兵等特性使其成为广受欢迎的缓存中间件。

在 Java 后端开发中,Redis 是面试常考的技术栈之一,是开发四大件(Java 基础、Spring Boot、MySQL、Redis)之一,因此在开发和学习中应扎实掌握 Redis 相关知识。

1 Redis 与 Spring Boot 的整合

  1. 添加依赖:在 pom.xml 中添加 Redis 依赖,Spring Boot 默认使用 Lettuce 作为 Redis 连接池,可避免频繁创建和销毁连接,提升应用性能和可靠性。

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置 Redis:在 application.yml 中进行配置,本地配置指定 host 和 port 即可,生产环境配置可参考相关教程。

    yaml 复制代码
    redis:
      host: localhost
      port: 6379
      password:
  3. 启动 Redis 服务 :以Window系统为例,使用终端工具启动 Redis 服务,可通过 redis-cli ping 检查 Redis 服务是否安装和运行正常,使用 redis-server 启动服务,默认端口为 6379。

  4. 编写测试类 :编写 Redis 测试类 RedisTemplateDemo,快速验证 Redis 在项目中的可用性。

    java 复制代码
    @SpringBootTest(classes = QuickForumApplication.class)
    public class RedisTemplateDemo {
        @Autowired
        private RedisTemplate<Object, Object> redisTemplate;
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Test
        public void testPut() {
            redisTemplate.opsForValue().set("itwanger", "沉默王二");
            stringRedisTemplate.opsForList().rightPush("girl", "陈清扬");
            stringRedisTemplate.opsForList().rightPush("girl", "小转玲");
            stringRedisTemplate.opsForList().rightPush("girl", "茶花女");
        }
    
        @Test
        public void testGet() {
            Object value = redisTemplate.opsForValue().get("itwanger");
            System.out.println(value);
            List<String> girls = stringRedisTemplate.opsForList().range("girl", 0, -1);
            System.out.println(girls);
        }
    }

@SpringBootTest(classes = QuickForumApplication.class) 注解指定项目启动类,@Autowired 注解注入 RedisTemplateStringRedisTemplateRedisTemplate 可操作任意类型数据,StringRedisTemplate 仅能操作字符串类型数据。testPut() 方法分别操作 Redis 中的字符串和列表类型数据,testGet() 方法获取相应数据。

2 在 Spring Boot 中使用 Redis 操作不同数据结构

  1. 字符串 :注入 RedisTemplate,使用 opsForValue() 方法获取操作对象,通过 set() 设置键值对,get() 获取值。

    java 复制代码
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }
    
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
  2. 列表 :注入 StringRedisTemplate,利用 opsForList() 方法获取操作对象,rightPush() 向列表右侧添加元素,range() 获取指定下标范围元素。

    java 复制代码
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    public void push(String key, String value) {
        stringRedisTemplate.opsForList().rightPush(key, value);
    }
    
    public List<String> range(String key, int start, int end) {
        return stringRedisTemplate.opsForList().range(key, start, end);
    }
  3. 哈希 :使用 opsForHash() 方法获取操作对象,put() 添加字段和值,get() 获取指定字段值。

    java 复制代码
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }
    
    public Object hget(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }
  4. 集合 :通过 opsForSet() 方法获取操作对象,add() 添加元素,members() 获取所有元素。

    java 复制代码
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    public void sadd(String key, String value) {
        stringRedisTemplate.opsForSet().add(key, value);
    }
    
    public Set<String> smembers(String key) {
        return stringRedisTemplate.opsForSet().members(key);
    }
  5. 有序集合 :使用 opsForZSet() 方法获取操作对象,add() 添加元素和分值,range() 获取指定下标范围元素。

    java 复制代码
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void zadd(String key, String value, double score) {
        redisTemplate.opsForZSet().add(key, value, score);
    }
    
    public Set<Object> zrange(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

3 技术派中的 Redis 实例应用

技术派使用 Redis 缓存用户 session 信息和 sitemap(用于帮助搜索引擎更好索引网站内容的 XML 文件)。

3.1 RedisClient 类

基于 RedisTemplate 封装,简化使用成本。代码路径:com.github.paicoding.forum.core.cache.RedisClient

ForumCoreAutoConfig 配置类通过构造方法注入 RedisTemplate,并调用 RedisClient.register(redisTemplate) 注册到 RedisClient 中,RedisTemplate 由 Spring Boot 自动配置机制注入。

java 复制代码
private static RedisTemplate<String, String> template;

public static void register(RedisTemplate<String, String> template) {
    RedisClient.template = template;
}
java 复制代码
public class ForumCoreAutoConfig {
    @Autowired
    private ProxyProperties proxyProperties;

    public ForumCoreAutoConfig(RedisTemplate<String, String> redisTemplate) {
        RedisClient.register(redisTemplate);
    }

3.2 用户 session 相关操作

  • 验证码校验成功后,调用 RedisClientsetStrWithExpire 方法存储 session,该方法使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法执行 Redis 命令,调用 RedisConnectionsetEx 方法设置键值对及过期时间。
java 复制代码
/**
 * 带过期时间的缓存写入
 *
 * @param key
 * @param value
 * @param expire s为单位
 * @return
 */
public static Boolean setStrWithExpire(String key, String value, Long expire) {
    return template.execute(new RedisCallback<Boolean>() {
        @Override
        public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
            return redisConnection.setEx(keyBytes(key), expire, valBytes(value));
        }
    });
}
  • 用户登出时,调用 del 方法删除 session。
java 复制代码
public static void del(String key) {
    template.execute((RedisCallback<Long>) con -> con.del(keyBytes(key)));
}
  • 用户登录时,调用 getStr 方法根据 session 获取用户 ID。
java 复制代码
public static String getStr(String key) {
    return template.execute((RedisCallback<String>) con -> {
        byte[] val = con.get(keyBytes(key));
        return val == null? null : new String(val);
    });
}

具体调用代码位于com.github.paicoding.forum.service.user.service.help.UserSessionHelper

java 复制代码
/**
 * 使用jwt来存储用户token,则不需要后端来存储session了
 */
@Slf4j
@Component
public class UserSessionHelper {
    @Component
    @Data
    @ConfigurationProperties("paicoding.jwt")
    public static class JwtProperties {
        /**
         * 签发人
         */
        private String issuer;
        /**
         * 密钥
         */
        private String secret;
        /**
         * 有效期,毫秒时间戳
         */
        private Long expire;
    }

    private final JwtProperties jwtProperties;

    private Algorithm algorithm;
    private JWTVerifier verifier;

    public UserSessionHelper(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
        algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
        verifier = JWT.require(algorithm).withIssuer(jwtProperties.getIssuer()).build();
    }

    public String genSession(Long userId) {
        // 1.生成jwt格式的会话,内部持有有效期,用户信息
        String session = JsonUtil.toStr(MapUtils.create("s", SelfTraceIdGenerator.generate(), "u", userId));
        String token = JWT.create().withIssuer(jwtProperties.getIssuer()).withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpire()))
                .withPayload(session)
                .sign(algorithm);

        // 2.使用jwt生成的token时,后端可以不存储这个session信息, 完全依赖jwt的信息
        // 但是需要考虑到用户登出,需要主动失效这个token,而jwt本身无状态,所以再这里的redis做一个简单的token -> userId的缓存,用于双重判定
        RedisClient.setStrWithExpire(token, String.valueOf(userId), jwtProperties.getExpire() / 1000);
        return token;
    }

    public void removeSession(String session) {
        RedisClient.del(session);
    }

    /**
     * 根据会话获取用户信息
     *
     * @param session
     * @return
     */
    public Long getUserIdBySession(String session) {
        // jwt的校验方式,如果token非法或者过期,则直接验签失败
        try {
            DecodedJWT decodedJWT = verifier.verify(session);
            String pay = new String(Base64Utils.decodeFromString(decodedJWT.getPayload()));
            // jwt验证通过,获取对应的userId
            String userId = String.valueOf(JsonUtil.toObj(pay, HashMap.class).get("u"));

            // 从redis中获取userId,解决用户登出,后台失效jwt token的问题
            String user = RedisClient.getStr(session);
            if (user == null || !Objects.equals(userId, user)) {
                return null;
            }
            return Long.valueOf(user);
        } catch (Exception e) {
            log.info("jwt token校验失败! token: {}, msg: {}", session, e.getMessage());
            return null;
        }
    }
}

3.3 sitemap 相关操作

  • 获取 sitemap 时,调用 hGetAll 方法,使用 RedisTemplateexecute 方法执行 Redis 命令,通过 RedisCallback 接口的 doInRedis 方法调用 RedisConnectionhGetAll 方法获取哈希表所有字段和值,并进行类型转换后返回。
java 复制代码
    public static <T> Map<String, T> hGetAll(String key, Class<T> clz) {
        Map<byte[], byte[]> records = template.execute((RedisCallback<Map<byte[], byte[]>>) con -> con.hGetAll(keyBytes(key)));
        if (records == null) {
            return Collections.emptyMap();
        }

        Map<String, T> result = Maps.newHashMapWithExpectedSize(records.size());
        for (Map.Entry<byte[], byte[]> entry : records.entrySet()) {
            if (entry.getKey() == null) {
                continue;
            }

            result.put(new String(entry.getKey()), toObj(entry.getValue(), clz));
        }
        return result;
    }
  • 添加文章时,调用 hSet 方法,使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法,根据值的类型转换为字符串后调用 RedisConnectionhSet 方法设置哈希表字段值,返回设置结果。
java 复制代码
public static <T> T hGet(String key, String field, Class<T> clz) {
    return template.execute((RedisCallback<T>) con -> {
        byte[] records = con.hGet(keyBytes(key), valBytes(field));
        if (records == null) {
            return null;
        }

        return toObj(records, clz);
    });
}
  • 移除文章时,调用 hDel 方法,使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法调用 RedisConnectionhDel 方法删除字段,返回删除结果。
java 复制代码
public static <T> Boolean hDel(String key, String field) {
    return template.execute(new RedisCallback<Boolean>() {
        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.hDel(keyBytes(key), valBytes(field)) > 0;
        }
    });
}
  • 初始化 sitemap 时,先调用 del 方法,再调用 hMSet 方法,hMSet 方法使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法调用 RedisConnectionhMSet 方法一次性设置多个哈希表字段值。
java 复制代码
/**
 * fixme: 加锁初始化,更推荐的是采用分布式锁
 */
private synchronized void initSiteMap() {
    long lastId = 0L;
    RedisClient.del(SITE_MAP_CACHE_KEY);
    while (true) {
        List<SimpleArticleDTO> list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE);
        // 刷新文章的统计信息
        list.forEach(s -> countService.refreshArticleStatisticInfo(s.getId()));

        // 刷新站点地图信息
        Map<String, Long> map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a));
        RedisClient.hMSet(SITE_MAP_CACHE_KEY, map);
        if (list.size() < SCAN_SIZE) {
            break;
        }
        lastId = list.get(list.size() - 1).getId();
    }
}

为提升搜索引擎对技术派的收录,开发了 sitemap 自动生成工具,在 SitemapServiceImpl 中通过定时任务每天 5:15 分刷新站点地图,确保数据一致性。

java 复制代码
 /**
  * 采用定时器方案,每天5:15分刷新站点地图,确保数据的一致性
  */
 @Scheduled(cron = "0 15 5 * * ?")
 public void autoRefreshCache() {
     log.info("开始刷新sitemap.xml的url地址,避免出现数据不一致问题!");
     refreshSitemap();
     log.info("刷新完成!");
 }

4 关于 RedisTemplate 的 execute 方法

RedisTemplateexecute(RedisCallback<T> action) 方法用于执行任意 Redis 命令,接收 RedisCallback 接口作为参数,将 Redis 连接传递给回调接口执行命令。

java 复制代码
@Nullable
public <T> T execute(RedisCallback<T> action) {
    return this.execute(action, this.isExposeConnection());
}

以下是测试用例示例:

java 复制代码
@Test
public void testExecute() {
    redisTemplate.execute(new RedisCallback<Object>() {
        @Override
        public Object doInRedis(RedisConnection connection) throws DataAccessException {
            connection.set("itwanger".getBytes(), "沉默王二".getBytes());
            byte[] value = connection.get("itwanger".getBytes());
            String strValue = new String(value);
            System.out.println(strValue);
            return null;
        }
    });
}

5 总结

Redis 是高性能的内存数据存储系统,支持多种数据结构。在 Spring Boot 中使用 Redis 作为缓存可提升应用性能和响应速度,通过 spring-boot-starter-data-redis 整合 Redis 操作简便。可使用 RedisTemplate 执行各种 Redis 命令,技术派使用 Redis 缓存 session 和 sitemap,应用了字符串和哈希表数据结构。未来将进一步介绍 Redis 其他数据结构和高级功能,如发布/订阅、事务、Lua 脚本等。

6 参考链接

  1. 技术派Redis的缓存示例
  2. 项目仓库(GitHub)
  3. 项目仓库(码云)
相关推荐
文牧之3 分钟前
Oracle 的 SEC_CASE_SENSITIVE_LOGON 参数
运维·数据库·oracle
平行绳6 分钟前
零基础玩转 Coze 数据库,看这篇就够了!
数据库·人工智能·coze
NineData39 分钟前
NineData云原生智能数据管理平台新功能发布|2025年5月版
数据库·云原生·oracle·devops·ninedata
不会编程的猫星人39 分钟前
Oracle杀进程注意事项
数据库·microsoft·oracle
GUIQU.43 分钟前
【Oracle】安装单实例
数据库·oracle
老胖闲聊1 小时前
Python Django完整教程与代码示例
数据库·python·django
践行见远1 小时前
django之请求处理过程分析
数据库·django·sqlite
行星0081 小时前
Postgresql常用函数操作
数据库·postgresql
程序员葵安1 小时前
【Java Web】9.Maven高级
java·数据库·后端·maven
海棠一号2 小时前
Android Settings 数据库生成、监听与默认值配置
android·数据库