# boot-redis **Repository Path**: liang-tian-yu/boot-redis ## Basic Information - **Project Name**: boot-redis - **Description**: 记录SpringBoot + Redis案例 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-17 - **Last Updated**: 2025-09-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## boot-redis

boot-redis

**记录SpringBoot + Redis案例** **功能特性** - redis实现session - redis键值缓存 - redisson实现限流 - redisson实现分布式锁 - redis实现验证码 - redis工具类实现业务标识 - redis实现订阅消息通知 [TOC] ## 快速开始 - application.yml 根据TODO提示修改mysql和redis配置信息 - 启动项目 访问地址:http://127.0.0.1:18088/api/doc.html - 在`RedisController`下测试请求 - 在src/main/java/*/redis文件夹下查看具体设计与实现 **tips** - redis在实际业务开发场景中习惯用形式以`项目名称:模块名:业务标识`作为key - 缓存值最好把对象集合等转为json字符串存储,减少内存空间 > 客户端可视工具推荐[QuickRedis](https://gitee.com/quick123official/quick_redis_blog/releases/) ## SpringBoot配置Redis ### 导入依赖 ``` org.springframework.session spring-session-data-redis org.springframework.boot spring-boot-starter-data-redis org.redisson redisson 3.17.5 ``` - application.yml ``` spring: # Redis redis: host: 127.0.0.1 password: # 数据库索引 默认0 database: 0 port: 6379 # 超时时间 Duration类型 3秒 timeout: 3S ``` ### Redis序列化 序列化的意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。 > RedisTemplate默认使用JDK原生序列化器,可读性差、内存占用大 redis序列化对比 | 名称 | 序列化效率 | 反序列化效率 | 占用空间 | 是否推荐使用 | | ------------------------------ | ---------- | ------------ | -------- | --------------------- | | StringRedisSerializer | 很高 | 很高 | 高 | 推荐给key进行序列化 | | Jackson2JsonRedisSerializer | 高 | 较高 | 偏高 | 推荐给value进行序列化 | | GenericFastJsonRedisSerializer | 高 | 较低 | 较低 | 推荐给value进行序列化 | 具体代码实现看`RedisConfig.java` ### Spring Data Redis Spring Data Redis整合封装了一系列数据访问的操作,Spring Data Redis则是封装了对Jedis、Lettuce这两个Redis客户端的操作,提供了统一的RedisTemplate来操作Redis。 | API | 返回值类型 | 说明 | | --------------------------- | --------------- | ------------------------- | | redisTemplate.opsForValue() | ValueOperations | 操作**String**类型数据 | | redisTemplate.opsForHash() | HashOperations | 操作**Hash**类型数据 | | redisTemplate.opsForList() | ListOperations | 操作**List**类型数据 | | redisTemplate.opsForSet() | SetOperations | 操作**Set**类型数据 | | redisTemplate.opsForZset() | ZSetOperations | 操作**SortedSet**类型数据 | ## Redis实战 ### redis+session [redis实现session](https://juejin.cn/post/7231788630306865211) ``` spring: session: # 生效时间为7天 timeout: 604800 store-type: redis ``` > store-type: redis 表示redis读写Session ### 键值缓存 > 缓存值最好把对象集合等转为json字符串存储,减少内存空间 > > 可通过定时任务或模拟触发缓存预热 - 测试 ``` @Test public void redisTemplateTest() { ValueOperations opsForValue = redisTemplate.opsForValue(); Book book = new Book(); book.setId(1L); book.setBookName("name1"); opsForValue.set("mybook", book); System.out.println(opsForValue.get("mybook")); } ``` - 实际应用 ``` public BaseResponse> listStudent(Integer pageNum,Integer pageSize,StudentQueryRequest studentQueryRequest) { Student studentQuery = new Student(); if (studentQueryRequest != null) { BeanUtils.copyProperties(studentQueryRequest, studentQuery); } //设置键值格式 String rediskey=String.format("demo:student:list"); ValueOperations valueOperations = redisTemplate.opsForValue(); // 如果有缓存,直接走缓存 Page studentPage= (Page) valueOperations.get(rediskey); if(studentPage!=null){ log.info("redis load success"); return ResultUtils.success(studentPage); } QueryWrapper queryWrapper = new QueryWrapper<>(studentQuery); studentPage = studentService.page(new Page<>(pageNum,pageSize),queryWrapper); //写缓存并设置过期时间 try { valueOperations.set(rediskey,studentPage,30000, TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("redis set key error"); } return ResultUtils.success(studentPage); } ``` > 封装工具类`RedisUtil`,具体可看代码实现 ### SpringCache > 注意启动类配置@EnableCaching > 注意配置Redis ```plain package com.lty.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; /** * spring缓存配置redis * @example 使用Spring @Cacheable注解: @Cacheable(cacheNames = "users", key = "#id") (缓存名为users,key为id) * @author lty */ @Slf4j @Configuration public class RedisCacheConfig extends CachingConfigurerSupport { // @Value("${app.cache.timeToLive:-1}") private Duration timeToLive = Duration.ofSeconds(3600); // 默认缓存时间为1小时 /** * 自定义序列化方式 * @param factory * @return */ @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // 解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping(new ObjectMapper().getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config.entryTtl(timeToLive)) .build(); return cacheManager; } /** * 异常处理 当Redis缓存相关操作发生异常时 打印日志 程序正常走 * @return */ @Override public CacheErrorHandler errorHandler() { CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() { @Override public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { log.warn("Redis occur handleCacheGetError:key: [{}]", key); } @Override public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { log.warn("Redis occur handleCachePutError:key: [{}];value: [{}]", key, value); } @Override public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { log.warn("Redis occur handleCacheEvictError:key: [{}]", key); } @Override public void handleCacheClearError(RuntimeException e, Cache cache) { log.warn("Redis occur handleCacheClearError"); } }; return cacheErrorHandler; } } ``` ## Redisson ### 序言 一个java操作redis的客户端,提供了大量的分布式数据集来简化对redis的操作和使用,让开发者像使用本地集合一样使用redis,完全感知不到redis的存在。 > 不使用redisson-spring-boot-starter,可能会造成框架冲突 - 导入依赖 ``` org.redisson redisson 3.17.5 ``` - 配置redisson ``` @Configuration @ConfigurationProperties(prefix = "spring.redis") @Data public class RedissonConfig { private String host; private String port; private int database; private String password; @Bean public RedissonClient redissonClient() { // 1. 创建配置 Config config = new Config(); String redisAddress = String.format("redis://%s:%s", host, port); config.useSingleServer().setAddress(redisAddress).setDatabase(database); // 判断是否有redis密码 if (password != null && password.length() > 0) { config.useSingleServer().setPassword(password); } // 2. 创建实例 RedissonClient redisson = Redisson.create(config); return redisson; } } ``` - 测试 ``` @org.junit.Test public void redissionTest() { RList rList = redissonClient.getList("test-list"); rList.add("hello"); System.out.println("rList: "+rList.get(0)); } ``` ### 限流器 - 例子 ``` // 1、 声明一个限流器 RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); // 2、 设置速率,5秒中产生3个令牌 rateLimiter.trySetRate(RateType.OVERALL, 3, 5, RateIntervalUnit.SECONDS); // 3、试图获取一个令牌,获取到返回true boolean getToken=rateLimiter.tryAcquire(); ``` - 实战 ``` @Slf4j @Component public class RedisRaterLimiter { @Resource private RedissonClient redisson; /** * 限流前缀 */ private String LIMIT_PRE="LIMIT:"; private RateLimiter guavaRateLimiter = RateLimiter.create(Double.MAX_VALUE); /** * 基于Redis令牌桶算法 * @param name 限流标识(基本为方法名+ip作为唯一标识) * @param rate 限制的数量 速率 * @param rateInterval 单位时间内(毫秒) * @return */ public Boolean acquireByRedis(String name, Long rate, Long rateInterval) { boolean getToken; try { RRateLimiter rateLimiter = redisson.getRateLimiter(this.LIMIT_PRE + name); rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.MILLISECONDS); getToken = rateLimiter.tryAcquire(); rateLimiter.expireAsync(rateInterval * 2, TimeUnit.MILLISECONDS); } catch (Exception e) { getToken = true; } return getToken; } } ``` - 测试 ``` public String getPort(HttpServletRequest request){ // 获取令牌,模拟5s内限流1次 Boolean token= redisRaterLimiter.acquireByRedis("getPort:"+IpInfoUtil.getIpAddr(request),1L,5000L); log.info(IpInfoUtil.getIpAddr(request)); if (!token) { throw new BusinessException(ErrorCode.OPERATION_ERROR,"等等吧"); } String str="success"; return str; } ``` **全局限流+注解** - 拦截器 ``` @Slf4j @Component public class LimitRaterInterceptor implements HandlerInterceptor { @Resource private RedisRaterLimiter redisRaterLimiter; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ip = IpInfoUtil.getIpAddr(request); // 注解限流 try { // 用bean,method,ip 当作限流标识(limit_name:com.lty.controller.IndexController_getIp_127.0.0.1) HandlerMethod handlerMethod = (HandlerMethod) handler; Object bean = handlerMethod.getBean(); Method method = handlerMethod.getMethod(); RateLimiterAop rateLimiter = method.getAnnotation(RateLimiterAop.class); if (rateLimiter != null) { String name = rateLimiter.name(); Long limit = rateLimiter.rate(); Long timeout = rateLimiter.rateInterval(); if (StrUtil.isBlank(name)) { name = StrUtil.subBefore(bean.toString(), "@", false) + "_" + method.getName(); } if (rateLimiter.ipLimit()) { name += "_" + ip; } log.info("limit_name:"+name); Boolean token3 = redisRaterLimiter.acquireByRedis(name, limit, timeout); if (!token3) { String msg = "当前访问人数太多啦,请稍后再试"; if (rateLimiter.ipLimit()) { msg = "你手速怎么这么快,请点慢一点"; } throw new BusinessException(ErrorCode.OPERATION_ERROR,msg); } } } catch (BusinessException e) { throw new BusinessException(ErrorCode.OPERATION_ERROR,e.getMessage()); } catch (Exception e) { } return true; } } ``` - 配置拦截器 ``` /** 配置拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册拦截器 InterceptorRegistration ir = registry.addInterceptor(limitRaterInterceptor); // 配置拦截的路径 ir.addPathPatterns("/**"); // 配置不拦截的路径 避免加载css也拦截 //ir.excludePathPatterns(ignoredUrlsProperties.getLimitUrls()); } ``` - 注解 ``` @Target(ElementType.METHOD) // 作用于方法上 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiterAop { /** * 自定义限流切入点 name 默认类引用路径+方法名 * @return */ String name() default ""; /** * 限流速率(时间间隔内最大请求个数) * @return */ long rate() default 5; /** * 速率间隔 单位毫秒 * @return */ long rateInterval() default 1000; /** * 是否启用IP限流(加上IP作为name标识) * @return */ boolean ipLimit() default false; } ``` - 注解使用 ``` @RateLimiterAop(rate = 1,rateInterval = 10000L,ipLimit = true) ``` ### 分布式锁 #### 问题 怎么保存同一时间只有1个服务器能抢到锁? 核心思想:先来的人先把数据改成自己的标识(服务器ip),后来的人发现标识已存在,就抢锁失败,继续等待。等先来的人执行方法结束,把标识清空,其他的人继续抢锁。 MYSQL:select for uodate 行级锁 redis:setnx(set if not exists) 不存在则设置,设置成功返回true,否则返回false - 注意事项 1. 用完锁要手动释放(方法要写在finally) 2. 锁一定要加过期时间 3. 如果方法执行时间过长,锁提前过期了? ​ 问题: ​ 连锁反应:释放掉别人的锁 ​ 会存在多个方法同时执行的情况 ​ **解决方案:续期** 1. 延长锁的过期时间:在获得锁之后,可以手动将锁的过期时间延长,以确保方法执行完成前锁不会过期。可以使用Redis的`expire`命令来延长锁的过期时间。 2. 使用守护线程:在获取锁之后,启动一个守护线程来定期更新锁的过期时间。这样即使方法执行时间过长,守护线程会定期为锁续期,确保锁不会提前过期。这种方式可以使用`Thread`或者线程池来实现。 3. 适当提高锁的过期时间:在设置锁的过期时间时,可以稍微增加一些额外的缓冲时间,以便应对方法执行时间变化或网络延时等情况。这样即使方法执行时间稍长,锁仍然可以在方法完成前保持有效。 4. 使用锁续约机制:在获取锁之后,可以使用一个定时任务定期去续约锁,即重置锁的过期时间,确保锁在方法执行完成前不会过期。可以通过设置一个定时任务或者周期性地调用Redis的`expire`命令来实现。 #### 实现 Redisson分布式锁的实现是基于Redis的SETNX命令和Lua脚本,具体的实现原理如下: 1.获取锁:当客户端请求获取锁时,Redisson会向Redis发送一个SETNX命令,尝试将一个特定的键(锁的标识)设置为一个特定的值(客户端标识),并设置锁的超时时间。 2.争用锁:如果多个客户端同时尝试获取同一个锁,只有一个客户端能够成功设置键的值,其他客户端的SETNX命令将失败,它们会继续尝试获取锁。 3.锁超时:为了防止某个客户瑞获取锁后发生异常导致锁永远不会被释放,Redisson设置了锁的超时时间。当锁的超时时间到达后,Redisson会自动释放锁,允许其他客户端获取锁。 4.释放锁:当客户端执行完锁保护的操作后,可以主动释放锁,这将删除锁的标识键,或者锁的自动超时也会导致锁的释放。 5.锁的可重入性:Redisson支持可重入锁,允许同一客户端多次获取同一个锁,然后多次释放锁。只有所有获取锁的次数都释放后,锁才会被完全释放 6.锁的续期:如果一个客户端在持有锁时,锁的超时时间即将到期,Redisson会自动为锁续期,防止锁在操作过程中被自动释放。 #### 模板 ``` public interface DistributedLockTemplate { /** * 执行方法 * @param lockId 锁id(对应唯一业务ID) * @param timeout 最大等待获取锁时间 * @param leaseTime 最长占用锁时间 <=0或null时将启用看门狗机制(程序未执行完自动续期锁) * @param unit 时间单位 * @param callback 回调方法 * @return */ Object execute(String lockId, Integer timeout, Integer leaseTime, TimeUnit unit, Callback callback); } ``` ## 验证码 验证码实现 以下是一些常见的解决方案: 1. 验证码有效期管理 设置验证码有效期:验证码接口在设计时可以设定验证码的有效期,例如设置为几分钟或几小时。一旦验证码超过有效期,它将自动失效。 动态更新验证码有效期:在每次用户请求验证码时,可以动态生成一个新的验证码,并更新其有效期。这样,即使先前的验证码仍在有效期内,新的验证码也会覆盖旧的。 2. 请求频率限制 限制请求次数:为了防止恶意请求或误操作,可以对每个用户或手机号设置一定时间段内的验证码请求次数限制。 滑动时间窗口:采用滑动时间窗口技术,对每个时间段内的请求次数进行统计,并根据统计结果决定是否允许新的请求。 3. 验证码与请求的绑定 唯一标识符:在生成验证码时,可以为每个验证码分配一个唯一的标识符(如UUID),并将其与用户的请求信息(如手机号、IP地址等)进行绑定。 请求验证:当用户提交验证码进行验证时,系统需要检查提交的验证码是否与之前的请求信息匹配,以确保验证码的有效性。 4. 缓存与存储管理 缓存机制:使用缓存机制来存储验证码及其相关信息,以提高系统的响应速度和效率。 存储清理:定期清理过期或无效的验证码信息,以释放存储空间并避免潜在的安全风险。 5. 安全措施 加密传输:确保验证码在传输过程中的安全性,防止被恶意截获或篡改。 日志记录:记录验证码的生成、发送、验证等关键操作日志,以便在出现问题时进行追踪和排查 ## -------------- ## Redis ### 基本知识 Redis(Remote Dictionary Server),即远程字典服务 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Vlue数据库,支持多种类型的数据结构存储系统。 共有16个数据库,默认使用的是第0个,使用select切换(select 3),DBSIZE(查看数据库大小) **作用** 1.内存存储、持久化,内存中是断电即失,所以持久化很重要(rdb、aof) 2.效率高,可以用于高速缓存 3.发布订阅系统 4.地图信息分析 5.计时器、计数器(浏览量) **特性** 1.多样的数据类型 2.持久化 3.集群 4.事务 ### 基本命令 ```bash 默认端口:6379 ping --测试连接返回PONG set name lty get name keys * type name #查看类型 exists name #判断存在 move name 1 #移动到1数据库 append name “hello” #追加字符 strlen key1 #获取字符串长度 expire name 10 #设置过期时间 ttl name #查看过期时间 flushall #清除所有数据库 fushdb #清除当前数据库 ``` ## 数据类型 ### String ```bash set views 0 incr views #自增1 incrby views 10 #步长 decr views #自减1 decrby views 10 #步长 GETRANGE key1 0 3 #截取字符串闭区间 SETRAGE key2 1 xx #替换指定位置字符串 ``` ``` #setex(set with expire) #设置过期时间 setex key 30 "hello" #设置值为hello,30秒后过期 #setnx(set if not exist) #不存在再设置(在分布式锁中会常常使用 ) setnx mykey "redis" #创建成功 setnx mykey "MongoDB" #创建失败 ``` ``` mset k1 v1 k1 v2 k3 v3 mget k1 k2 k3 mset user:1:name zhangsan user:1:age 2 mget user:1:name user:1:age ``` ``` #getset 先get后set getset db redis get db mongodb #结果为mongodb 如果存在值,获取原来的值,并设置新的值 ``` ### List 在redis中,可以把list玩成栈,队列,阻塞队列(双向) ``` LPUSH list one #往左放 LRANGE list 0 -1 Lpop list lindex list 1 Llen list #返回列表长度 lrem list 1 one #移除一个value值 ltrim mylist 1 2 #截断 LINSERT mylist before hello World ``` 消息队列(Lpush Rpop), 栈(Lpush Lpop) ### Set 值不重复 ``` sadd myset "hello" SMEMBERS myset #查看 SISMEMBER myset hello #判断元素 scard myset #元素个数 srem myset hello #移除 SRANDMEMBER myset [个数] #随机抽选元素 spop myset #随机删除集合中的元素 sdiff key1 key2 #差集 sinter key1 key2 #交集 sunion key1 key2 #并集 #共同关注,共同爱好 ``` ### Hash(哈希) Map集合,key-map 这个值是一个map集合 ```bash hset myhash field1 lty hget myhash field1 hmset myhash ... hgetall myhash hdel myhash field1 #删除 hkeys myhash hvals myhash ``` ### Sorted Set(有序集合) 在set的基础上,增加了一个值 ```bash zadd salary 500 zhangsan ZRANGEBYSCORE salary -inf +inf #最小到最大排序 zrem salary zhangsan ``` ### geospatial地理位置 ``` geoadd China:city 116.40 39.90 beijing geoadd China:city 121.47 31.23 shanghai geopos china:city beijing #获取指定城市经纬度 GEODIST #两点距离 ``` ### Hyperloglog 基数(不重复的元素个数) ``` PFadd PFmerge PFcount ``` ### Bitmaps 位存储 ``` setbit sign k1 1 getbit sign k1 bitcounnt sign k1 ``` ## Redis安装 - 解压 ``` tar -zvxf redis-5.0.7.tar.gz ``` - 编译(文件夹为redis) ``` cd到/usr/local/redis目录,输入命令make执行编译命令 ``` - 安装 ``` make PREFIX=/usr/local/redis install ``` - 启动(先配置好redis.conf) ``` #src目录下 ./redis-server ../redis.conf & #登录 auth [password] ps -aux | grep redis ``` - 配置redis-cli环境变量 ``` #复制redis-cli文件 sudo cp src/redis-cli /usr/local/bin/ ``` redis-flushdb.sh ``` #!/bin/bash #可以用第一个参数指定需求清除的库 db=0 if [ -n "$1" ];then db=$1 fi redis-cli -h localhost -p 6379 -a liang111 < > > protected-mode是redis本身的一个安全层,这个安全层的作用:就是只有【本机】可以访问redis,其他任何都不可以访问redis。这个安全层开启必须满足三个条件,不然安全层处于关闭状态: > > (1)protected-mode yes(处于开启) > > (2)没有bind指令。原文:The server is not binding explicitly to a set of > addresses using the “bind” directive. > > (3)没有设置密码。原文:No password is configured。 > > 这时redis的保护机制就会开启。开启之后,只有本机才可以访问redis。 如果上面三个条件任何一个不满足,就不会开启保护机制。 > > 1.修改 protected-mode yes 改为:protected-mode no > > 2.注释掉 #bin 127.0.0.1 > > 3.密码设置(必须设置) > > ​ requirepass 123456 --- > Redis自启动(windows) 在redis所在的目录下 ```plain redis-server.exe --service-install redis.windows.conf --loglevel verbose ``` 查看一下Redis服务是否注册,Win+R输入services.msc,确定进入,在查找是有Redis tips: 可添加redis目录环境变量 ps: 常用的redis服务命令(需要在从cmd进入redis目录下执行): 卸载服务:redis-server --service-uninstall 开启服务:redis-server --service-start 停止服务:redis-server --service-stop