麒麟v10 上部署 TiDB v5.1.2 生产环境优化实践
979
2023-04-17
SpringBoot整合Redis实现分布式缓存、分布式锁等,实战分享!
一、摘要
在前几篇文章中,我们详细介绍了 redis 的一些功能特性以及主流的 java 客户端 api 使用方法。
在当前流行的微服务以及分布式集群环境下,Redis 的使用场景可以说非常的广泛,能解决集群环境下系统中遇到的不少技术问题,在此列举几个使用 redis 经常用到的功能!
分布式缓存:在分布式的集群架构中,将缓存存储在内存中会出现很多的问题,比如用户回话信息,因为这部分信息需要与其他机器共享,此时利用 Redis 可以很好的解决机器之间数据共享的问题,缓存也是 Redis 中使用最多的场景分布式锁:在高并发的情况下,我们需要一个锁来防止并发带来的脏数据,Java 自带的锁机制显然对进程间的并发并不好使,此时利用 Redis 的单线程特性,实现分布式锁控制接口限流:在集群环境下,可以利用 redis 的分布式自增ID功能,精准的统计每个接口在指定时间内的请求次数,利用这个特性,可以定向限制某个接口恶意频刷
当然 Redis 的使用场景并不仅仅只有这么多,还有很多未列出的场景,如发布/订阅,分布锁集合等。
现实中我们大部分的微服务项目,都是基于 SpringBoot 框架进行快速开发,在 SpringBoot 项目中我们应该如何使用 Redis 呢?
代码实践如下!
二、代码实践
2.1、添加 redis 相关依赖包
实际上,在 SpringBoot 项目中,使用redis非常简单,开发者只需要在项目中添加如下的依赖即可!
在之前的 redis 系列文章中,我们知道官方推荐的 java 版本的 redis 客户端,一共有三个,分别是Jedis、Lettuce和Redisson,其中大部分场景下,使用Jedis或者Lettuce就足够了。
在 SpringBoot 1.x 版本里面,spring-boot-starter-data-redis默认集成的客户端是Jedis;从 SpringBoot 2.x 开始,spring-boot-starter-data-redis默认集成的客户端是Lettuce。
以springBoot-2.1.0版本为例,我们打开spring-boot-starter-data-redis依赖配置,核心配置如下!
可以很清晰的看到,spring-boot-starter-data-redis默认集成的客户端是Lettuce。
2.2、配置 redis 相关连接信息
依赖包添加完成之后,我们还需要在application.properties全局配置文件中,添加相关的 redis 配置信息。
# Redis数据库索引(默认为0)spring.redis.database=0# Redis服务器地址spring.redis.host=127.0.0.1# Redis服务器连接端口spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password=# 连接池最大连接数(使用负值表示没有限制) 默认 8spring.redis.lettuce.pool.max-active=8# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1spring.redis.lettuce.pool.max-wait=-1# 连接池中的最大空闲连接 默认 8spring.redis.lettuce.pool.max-idle=8# 连接池中的最小空闲连接 默认 0spring.redis.lettuce.pool.min-idle=0
最后,我们来跑一个最简单的单元测试,看看是否能联通(确保 redis 的服务端已经启动)。
@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void test() throws Exception { String uuid = UUID.randomUUID().toString(); stringRedisTemplate.opsForValue().set(uuid, uuid, 60, TimeUnit.SECONDS); System.out.println(stringRedisTemplate.opsForValue().get(uuid));}
如果控制台输出正常,说明基本配置已经完成,如果有错误,看错误信息然后依次排查!
2.3、重新配置 RedisTemplate 的序列化策略
SpringBoot 为我们提供了一个高度封装的RedisTemplate类来操作redis的各个命令,开发者无需关心具体的客户端 api 问题,通过RedisTemplate提供的方法,就可以操作redis,方便开发者可以无成本替换 java 客户端。
当我们存储对象的时候,RedisTemplate默认采用的是 Jdk 提供的序列化工具库,该工具有个要求,缓存的对象必须显式实现序列化接口,才能保存。
通常情况下,我们会自定义RedisTemplate的序列化策略,采用Jackson将对象转成json,查询的时候将json转成对象。
具体实现如下:
@Configurationpublic class RedisTemplateConfig { /** * 默认是JDK的序列化策略,这里配置redisTemplate采用的是Jackson2JsonRedisSerializer的序列化策略 * @param factory * @return */ @Bean public RedisTemplate
2.4、RedisTemplate 使用介绍
我们知道,redis 提供的数据结构很丰富,支持字符串、哈希表、列表、集合、有序集合等数据类型的存储,RedisTemplate对这五种数据结构分别定义了不同的操作类,具体如下:
ValueOperations:操作最简单的K-V数据ListOperations:操作list类型的数据HashOperations:操作hash类型的数据SetOperations:操作set类型的数据ZSetOperations:操作zset类型的数据
相关的 api 操作如下!
2.4.1、操作字符串的 api
2.4.2、操作对象的 api
public class UserVo { private String email; private String name; public String getEmail(){ return email; } public void setEmail(String email){ this.email = email; } public String getName(){ return name; } public void setName(String name){ this.name = name; } public UserVo(String email, String name){ this.email = email; this.name = name; } public UserVo(){ } @Override public String toString(){ return "UserVo{" + "email='" + email + '\'' + ", name='" + name + '\'' + '}'; }}
2.4.3、操作列表的 api
2.4.4、操作哈希的 api
@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void test() throws Exception { // 向hash中添加数据 HashOperations
2.4.5、操作集合的 api
@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void test() throws Exception { // 向集合中添加数据 SetOperations
2.4.6、操作有序集合的 api
@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void test() throws Exception { // 向有序集合中添加数据 ZSetOperations
2.4.7、操作分布锁相关的 api
@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void test(){ //分布式自增ID for (int i = 0; i < 10; i++) { Long incrementId = stringRedisTemplate.opsForValue().increment("orderId"); //设置2秒后自动过期 stringRedisTemplate.expire("orderId", 2, TimeUnit.SECONDS); System.out.println("orderId当前值:" + incrementId); } //分布式加锁,5秒自动过期 boolean lock = lock("LOCK", "test", 5L); System.out.println("加锁结果:" + lock); boolean unlock = releaseLock("LOCK", "test"); System.out.println("解锁结果:" + unlock);}/** * 直接加锁 * @param key * @param value * @param expire * @return */public boolean lock(String key,String value, Long expire){ String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end"; RedisScript
2.5、如果要换成 jedis,如果更换?
从 SpringBoot 2.x 开始,spring-boot-starter-data-redis默认集成的客户端是Lettuce,但是有的项目使用了Jedis依赖包相关的代码,如何无缝替换呢?
在pom.xml文件中,添加Jedis依赖包,排除相关的包即可,示例代码如下:
最后,在application.properties中,添加jedis相关配置,内容如下:
# Redis数据库索引(默认为0)spring.redis.database=1# Redis服务器地址spring.redis.host=127.0.0.1# Redis服务器连接端口spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password=# Redis服务器连接超时配置spring.redis.timeout=1000# 连接池配置spring.redis.jedis.pool.max-active=8spring.redis.jedis.pool.max-wait=1000spring.redis.jedis.pool.max-idle=8spring.redis.jedis.pool.min-idle=0spring.redis.jedis.pool.time-between-eviction-runs=100
2.6、手动封装一个分布式锁实现类
默认情况下,无论是Jedis还是Lettuce,都没有为我们提供redis分布式锁的实现,因此我们自己进行封装,当然你也可以直接添加Redisson包,里面也提供了分布式锁实现的相关 API。
如果当前 redis 是单机环境,或者哨兵模式,我们完全可以自行封装一个分布式锁实现类,具体代码如下:
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.data.redis.core.script.RedisScript;import org.springframework.stereotype.Component;import java.time.Duration;import java.util.Collections;/** * redis分布式锁服务类 * 采用LUA脚本实现,保证加锁、解锁操作原子性 */@Componentpublic class RedisLockService { /** * 分布式锁过期时间,单位秒 */ private static final Long DEFAULT_LOCK_EXPIRE_TIME = 60L; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 尝试在指定时间内加锁 * @param key * @param value * @param timeout 锁等待时间 * @return */ public boolean tryLock(String key,String value, Duration timeout){ long waitMills = timeout.toMillis(); long currentTimeMillis = System.currentTimeMillis(); do { boolean lock = lock(key, value, DEFAULT_LOCK_EXPIRE_TIME); if (lock) { return true; } try { Thread.sleep(1L); } catch (InterruptedException e) { Thread.interrupted(); } } while (System.currentTimeMillis() < currentTimeMillis + waitMills); return false; } /** * 直接加锁 * @param key * @param value * @param expire * @return */ public boolean lock(String key,String value, Long expire){ String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end"; RedisScript
三、关于 key 的设计
通常情况下,我们对key采用如下方式进行设计,以便与其他项目中的key错开,避免发生冲突!
固定前缀:项目名:数据库名:表名:字段名:具体的值
其次,无论什么时候,只要有可能就利用key超时的优势,尽可能避免数据永久存储,因为一旦所有的key都永久存储,大量无效的key,会服务器资源非常严重不足,甚至不可用!
四、小结
本文主要围绕在 SpringBoot 项目中,如何集成 redis 并正确使用进行了简单的分享,内容难免有缺漏,欢迎网友留言指出!
五、参考
1、博客园 - 卡斯特梅的雨伞 - springboot中RedisTemplate的使用
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。