# 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