一,Redis快速入门
redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value结构的NoSQL型数据库。
1.SQL和NoSQL的区别:
2.特征:
1.键值(key-value)型,value支持多种不同数据结构,功能丰富
2.单线程,每个命令具备原子性
3.低延迟,速度快(基于内存、IO多路复用、良好的编程)
4.支持数据持久化
5.支持主从集群、分片集群
6.支持多语言客户端
二,Redis的安装(Linux)
1.安装Redis依赖
Redis 是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖:
yum install -y gcc tcl
2.上传安装包并解压
安装包下载地址:https://redis.io/download/
3.解压:
tar -zxvf redis-6.2.6.tar.gz
进入redis目录
cd redis-6.2.6
4.运行编译命令:
make && make install
默认的安装路径是在/usr/local/bin 目录下
5.启动redis-默认启动
redis-server
6.启动redis-指定配置启动
6.1先将配置文件备份一份
cp redis.conf redis.conf.bck
6.2修改redis.conf文件中的一些配置:
vi redis.conf
7.开机自启
7.1首先新建一个系统服务文件
vi /etc/systemd/system/redis.service
内容如下:
7.2让它生效需重载系统服务
systemctl daemon-reload
7.3启动
7.4查看进程
ps-ef |grep redis
7.5执行下面的命令,可以让redis
systemctl enable redis
三,Redis客户端
1.Redis客户端
Redis 安装完成后就自带了命令行客户端:redis-cli,使用方式如下:
redis-cli[options][commonds]
2.图形化桌面客户端
在下面这个仓库可以找到免费的安装包:
https://github.com/lework/RedisDesktopManager-Windows/releases
双击运行
redis默认有16个库
四,Redis命令
1.Redis数据结构介绍
Redis为了方便学习,将操作不同数据类型的命令也做了分组,在官网(https://redis.io/commands)可以查看到不同的命令:
2.Redis通用命令
通用指令是部分数据类型的,都可以使用的指令,常见的有:
keys:查看符合模板的所有key,不建议在生产环境设备上使用
del:删除一个指定的key,返回interger删除的个数
exist:判断key是否存在,返回1是,返回0否
expire:给一个key设置有效期,有效期到期时改key会自动删除
ttl:查看一个key的剩余有效期
通过help[command] 可以查看一个命令的具体用法,例如:
3.String类型
4.key的层级结构
5.Hash类型
6.List类型
7.Set类型
8.SortedSet
五 ,Redis客户端
1.Redis的java客户端
客户端网站:https://redis.io/clients
2.三种常见实现的优缺点
六, Jedis
Jedis的官网地址:https://github.com/redis/jedis
1.流程
2.基本步奏:
3.配置连接池
七,SpringDataRedis
官网地址:https://spring.io/projects/spring-data-redis
1.定义
2.快速入门
3.流程
4.基本步奏
5.自动序列化和反序列化
因为自动序列化的@class占用了额外的空间
6.手动序列化方式:
7.两种序列化实践方案
8.hash案例:
八,实战-session实现短信登入
注意:在当前目录中输入cmd点击回车可以进入 此目录对应窗口
1.设计
2.发送验证码
3.登入
4.实现登录效验拦截
前置拦截:
后置拦截-关闭
5.配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/upload/**",
"/user/code",
"/user/login"
);
}
}
6.隐藏用户登入信息
降低session存放的数据量,将user变为userDto,修改如下
service层业务代码如下:
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
//验证码发送
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.效验手机号
if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
//2.不符合 返回错误
return Result.fail("手机号格式错误");
}
//3.生成验证码 6位
String code= RandomUtil.randomNumbers(6);
//4.保存验证码
session.setAttribute("code",code);
//5.发送验证码
log.info("发送验证码成功:"+code);
//返回ok
return Result.ok();
}
//登入
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.效验手机号
String phone=loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
//不符合 返回错误
return Result.fail("手机号格式错误");
}
//2.效验验证码
String code=loginForm.getCode();
Object cacheCode= session.getAttribute("code");
if(RegexUtils.isCodeInvalid(code)|| StrUtil.isBlank(code)||!code.equals(cacheCode.toString())){
//不符合 返回错误
return Result.fail("验证码错误");
}
//3.判断客户是否存在
User user=query().eq("phone",phone).one();
if(user==null){
//4.如果不存在,创建新用户并保存
user= createUserWithPhone(phone);
}
// 5.将用户存入session
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class ));
return null;
}
//注册
private User createUserWithPhone(String phone) {
User user= new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME+RandomUtil.randomString(10));
return user;
}
}
拦截器代码如下:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TODO 1.获取session
HttpSession session = request.getSession();
//TODO 2.获取session中的用户
UserDTO user = (UserDTO)session.getAttribute("user");
//TODO 3.判断用户是否存在
if(user==null){
//TODO 4.不存在,拦截
response.setStatus(401);
return false;
}
//TODO 5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//关闭
UserHolder.removeUser();
}
}
拦截器配置代码如下:
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/upload/**",
"/user/code",
"/user/login"
);
}
}
Threadlocal代码如下
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
5.session共享问题的替代方案-redis
九,实战-redis实现短信登入
1.设计
2.前端实现
登入时会将后端给的token放入sessionStorage中,然后定义拦截器去拦截,每次发请求都拦截,携带token
3.后端实现
需要刷新token过期时间,否则以上代码会造成客户登入30分钟后过期,无法监听客户操作后刷新token过期时间
service层业务代码如下:
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
//验证码发送
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.效验手机号
if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
//2.不符合 返回错误
return Result.fail("手机号格式错误");
}
//3.生成验证码 6位
String code= RandomUtil.randomNumbers(6);
//4.保存验证码到redis
// session.setAttribute("code",code);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.发送验证码
log.info("发送验证码成功:"+code);
//返回ok
return Result.ok();
}
//登入
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.效验手机号
String phone=loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
//不符合 返回错误
return Result.fail("手机号格式错误");
}
//2.从redis获取验证码并效验
//String code=loginForm.getCode();
String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
Object cacheCode= session.getAttribute("code");
if(RegexUtils.isCodeInvalid(code)|| StrUtil.isBlank(code)||!code.equals(cacheCode.toString())){
//不符合 返回错误
return Result.fail("验证码错误");
}
//3.判断客户是否存在
User user=query().eq("phone",phone).one();
if(user==null){
//4.如果不存在,创建新用户并保存
user= createUserWithPhone(phone);
}
//5.保存用户信息到redis中
//6.随机生成token,作为登入令牌
String token= UUID.randomUUID().toString();
//7.将user对象作为map存储
UserDTO userDTO=BeanUtil.copyProperties(user,UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()//stringRedisTemplate的参数必须为String
,CopyOptions.create().ignoreNullValue().setIgnoreNullValue(true)//将userDTO中第一个属性id由integet转为String
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
//8.存储
String tokenKey=LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//9.设置token有效期
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
//10返回token
return Result.ok(token);
}
//注册
private User createUserWithPhone(String phone) {
User user= new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME+RandomUtil.randomString(10));
return user;
}
}
拦截器代码
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TODO 1.获取请求头中的token
String token=request.getHeader("authorization");
if(StrUtil.isBlank(token)){
response.setStatus(401);
return false;
}
//2.基于token获取redis中的用户
String key=LOGIN_USER_KEY+token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3.判断用户是否存在
if(userMap==null){
//4.不存在,拦截
response.setStatus(401);
return false;
}
//5.将查询到的hash数据转成UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.将对象存入ThreadLocal中
UserHolder.saveUser(userDTO);
//7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
总结:
十,实战-缓存
1.什么是缓存
缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高.
优缺点:
2.添加Redis缓存
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key=CACHE_SHOP_KEY+id;
//1.从redis中查询商品缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断商品缓存是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回商品
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.不存在,根据去数据库中查询
Shop shop = getById(id);
//5.数据库查询也不存在,返回错误
if (shop==null) {
return Result.fail("该店铺不存在");
}
//6.数据库查询存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7.返回
return Result.ok(shop);
}
}
3.缓存更新策略
第三种方案不能完全保证一致性,综上第一种方案较为实用.
这种情况下线程穿插时会触发异常,导致缓存和数据库数据不一致,可能性很高
这种情况下线程穿插时会触发异常,导致缓存和数据库数据不一致,可能性很低.
4.缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库.
(1)缓存空对象 解决缓存穿透问题
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key=CACHE_SHOP_KEY+id;
//1.从redis中查询商品缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断商品缓存是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回商品
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if(shopJson!=null){
return Result.fail("店铺信息不存在!");
}
//4.不存在,根据去数据库中查询
Shop shop = getById(id);
//5.数据库查询也不存在,返回错误
if (shop==null) {
//将空值写入redis,过期时间短一点 2分钟
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("该店铺不存在");
}
//6.数据库查询存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return Result.ok(shop);
}
5.缓存雪崩
缓存雪崩是指同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力.
6.缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击.
优缺点:
(1)基于互斥锁方式解决缓存击穿问题:
@Override
public Result queryById(Long id) {
//缓存击穿
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop==null) {
return Result.fail("店铺信息不存在");
}
return Result.ok(shop);
}
//缓存击穿互斥锁
public Shop queryWithMutex(Long id){
String key=CACHE_SHOP_KEY+id;
//1.从redis中查询商品缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断商品缓存是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回商品
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的是否是空值
if(shopJson!=null){
return null;
}
//4.实现缓存重建
//4.1.获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
//4.2.判断是否获取成功
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
if(!isLock){
//4.3.失败则休眠并重试
Thread.sleep(50);
queryWithMutex(id);
}
//4.不存在,根据去数据库中查询
shop = getById(id);
//5.数据库查询也不存在,返回错误
if (shop==null) {
//将空值写入redis,过期时间短一点 2分钟
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.数据库查询存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//7.释放互斥锁
unLock(lockKey);
}
//8.返回
return shop;
}
互斥锁的获取和释放
//获取互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MILLISECONDS);
return BooleanUtil.isTrue(flag);//自动拆箱
}
//释放互斥锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
(2)基于逻辑过期方式解决缓存击穿问题:
@Override
public Result queryById(Long id) {
//缓存击穿
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
//Shop shop = queryWithMutex(id);
//逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
if (shop==null) {
return Result.fail("店铺信息不存在");
}
return Result.ok(shop);
}
//缓存击穿-逻辑过期
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire(Long id){
String key=CACHE_SHOP_KEY+id;
//1.从redis中查询商品缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//5.1.未过期,直接返回店铺信息
return shop;
}
//5.2.已过期,需要缓存重建
//6.缓存重建
//6.1.获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//6.2.判断是否获取锁成功
if(isLock){
//6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6.4.返回过期的商品信息
return shop;
}
//重建缓存
public void saveShop2Redis(Long id,Long expireSeconds){
//1.查询店铺数据
Shop shop = getById(id);
//2.封装逻辑过期时间
RedisData redisData=new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
//获取互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MILLISECONDS);
return BooleanUtil.isTrue(flag);//自动拆箱
}
//释放互斥锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
7.缓存工具封装
方法1和方法3处理缓存穿透问题,
方法2和方法4处理热点key缓存击穿问题
(1)用工具类解决缓存穿透问题:
service层代码:
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
//缓存击穿
//Shop shop = queryWithPassThrough(id);
//用工具类解决缓存击穿
Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,30L,TimeUnit.MINUTES);
if (shop==null) {
return Result.fail("店铺信息不存在");
}
return Result.ok(shop);
}
工具类代码:
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//放入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
public <R,ID>R queryWithPassThrough(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack,Long time,TimeUnit unit){ //R和ID都是泛型
String key=keyPrefix+id;
//1.从redis中查询商品缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断商品缓存是否存在
if(StrUtil.isNotBlank(json)){
//3.存在,返回商品
R r = JSONUtil.toBean(json, type);
return r;
}
//判断命中的是否是空值
if(json!=null){
return null;
}
//4.不存在,根据去数据库中查询
R r = dbFallBack.apply(id);
//5.数据库查询也不存在,返回错误
if (r==null) {
//将空值写入redis,过期时间短一点 2分钟
stringRedisTemplate.opsForValue().set(key,"",time, unit);
return null;
}
//6.数据库查询存在,写入redis
this.set(key,r,time,unit);
//7.返回
return r;
}
}
(2)用工具类的逻辑过期解决缓存击穿问题:
service层:
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
//逻辑过期解决缓存击穿
//Shop shop = queryWithLogicalExpire(id);
//用工具类解决缓存击穿
Shop shop=cacheClient.queryWithLogicalExpire(
CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
if (shop==null) {
return Result.fail("店铺信息不存在");
}
return Result.ok(shop);
}
工具类代码:
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//放入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack,Long time,TimeUnit unit){
String key=keyPrefix+id;
//1.从redis中查询商品缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(json)){
//3.存在,返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期 if(expireTime.isAfter(LocalDateTime.now())){
//5.1.未过期,直接返回店铺信息
return r;
}
//5.2.已过期,需要缓存重建
//6.缓存重建
//6.1.获取互斥锁
String lockKey=keyPrefix+id;
boolean isLock = tryLock(lockKey);
//6.2.判断是否获取锁成功
if(isLock){
//6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存,查询数据库
R r1 = dbFallBack.apply(id);
//写入Redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6.4.返回过期的商品信息
return r;
}
//获取互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MILLISECONDS);
return BooleanUtil.isTrue(flag);//自动拆箱
}
//释放互斥锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}