黑马点评学习笔记
一、项目简介
- redis的企业应用实战,几乎涵盖所有的redis开发
二、项目架构和模块
项目架构
项目模块
三、模块开发
项目部署
- 导入半成品项目
- 创建数据库hmdp,然后使用sql文件创建数据库表
- 修改mysql、redis地址
- 启动后端项目、前端项目(部署在nginx上)
短信登录
1.基于Session实现登录
1.发送验证码
// 发送手机验证码
@Override
public Result sendCode(String phone, HttpSession session) {
// 1. 验证手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2. 手机号不合法,返回错误信息
return Result.fail("手机号非法,请重新输入!");
}
// 3. 手机号合法,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4. 保存验证码到session中
session.setAttribute(SESSION_PHONE_CODE, code);
// 5. 发送验证码(模拟)
log.debug("手机验证码发送成功!验证码是:{}", code);
// 6.返回成功结果
return Result.ok();
}
2.短信验证码登录、注册
// 登录、注册一体
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 校验手机号
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
// 2. 手机号不合法,返回错误信息
return Result.fail("手机号非法,请重新输入!");
}
// 2. 校验验证码
Object code = session.getAttribute(SESSION_PHONE_CODE);
String code1 = loginForm.getCode();
if (code == null || !code.toString().equals(code1)) {
// 3. session中没有验证码,或者验证码不正确,返回错误信息
return Result.fail("验证码错误!");
}
// 4. 验证码正确,查询手机号用户
User user = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
// 5. 手机号用户不存在,注册
if (user == null) {
user = createUserByPhone(loginForm.getPhone());
}
// 6. 将用户保存到session中,要转成UserDTO保护信息安全(不管是否查询到用户,最终user一定有用户,需要保存到session)
session.setAttribute(SESSION_USERDTO, BeanUtil.copyProperties(user, UserDTO.class));
// 7. 返回成功结果(不需要返回登录凭证,因为每次用户访问都自带cookie,cookie中有sessionid就可以判断登录状态了)
return Result.ok();
}
3.校验登录状态
创建拦截器
// mvc拦截器
public class LoginInterceptor implements HandlerInterceptor {
// 前置拦截(主要是校验登录状态)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从session中获取用户
HttpSession session = request.getSession();
Object user = session.getAttribute(SESSION_USERDTO);
// 2. 用户不存在,拦截
if (user == null) {
response.setStatus(401);//设置状态码
return false;//拦截
}
// 3. 用户存在,保存到TreadLocal线程中
UserHolder.saveUser((UserDTO) user);
// 4. 放行
return true;
}
// 后置拦截(移除线程中的用户,防止内存泄露)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
创建mvc配置类,添加拦截器并配置拦截路径
// webmvc配置类
@Configuration
public class MvcConfig implements WebMvcConfigurer {
// 添加自定义拦截器,并配置拦截规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
校验接口返回用户信息
/**
* 获取当前登录的用户并返回
* @return
*/
@GetMapping("/me")
public Result me(){
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
2.集群的session共享问题
-
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
-
session的替代方案(使用redis替代)应该满足:
-
数据共享
-
内存存储
-
key、value结构
-
-
Redis代替session需要考虑的问题:
-
选择合适的数据结构
-
选择合适的key
-
选择合适的存储粒度
-
3.基于Redis实现登录
使用redis存储数据考虑两个点
- 使用哪种数据结构,如验证码使用字符串存储,用户对象可以使用字符串或者hash存储,字符串存储对象比较直观但是仅限数据量不大,推荐使用hash方式,修改数据啥的更方便。。
- 键的设计,键的设计需要满足唯一性、可传递性,因为多台tomcat访问同一个redis,必须要保证唯一。后续我们需要在代码中获取redis的值,所以我们需要携带键来获取值,这里就需要传递性。如验证码使用手机号作为键,用户对象使用用户登录后的token作为键。
很多小技巧
-
StrUtil.isBlank(token)判断token是否为空
-
对象转map,并配置转换条件,如忽略null值、编辑字段,把值都转换成String类型。
Map<String, Object> usermap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() .ignoreNullValue().setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
-
map转对象,设置转换时不忽略错误,有异常就抛出
UserDTO userDTO = BeanUtil.fillBeanWithMap(usermap, new UserDTO(), false);//不忽略错误
-
生成简单token,没有横线
String token = UUID.randomUUID().toString(true);//没有短横线的uuid
-
把静态常量都专门写到一个类里,写成静态常量
-
redis存值时需要设置过期时间
// 设置有效期,因为hash不能存的时候设置有效期,所以只能后续设置 stringRedisTemplate.expire(tokenkey,LOGIN_USER_TTL,TimeUnit.MINUTES);//token在用户没有访问时,半个小时就过期
-
使用两个拦截器分工协作,可以达到用户点击任何页面就刷新token,防止token过期而需要重新登录的现象。
-
当自己创建的对象需要使用spring管理的对象时,不能直接注入,可以通过构造方法传入。
-
发送验证码
@Autowired private StringRedisTemplate stringRedisTemplate; // 发送手机验证码 @Override public Result sendCode(String phone, HttpSession session) { // 1. 验证手机号 if (RegexUtils.isPhoneInvalid(phone)) { // 2. 手机号不合法,返回错误信息 return Result.fail("手机号非法,请重新输入!"); } // 3. 手机号合法,生成验证码 String code = RandomUtil.randomNumbers(6); // 4. 保存验证码到redis中,手机号为键,验证码为值,两分钟后过期 stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5. 发送验证码(模拟) log.debug("手机验证码发送成功!验证码是:{}", code); // 6.返回成功结果 return Result.ok(); }
-
短信验证码登录、注册
// 登录、注册一体 @Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1. 校验手机号 if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) { // 2. 手机号不合法,返回错误信息 return Result.fail("手机号非法,请重新输入!"); } // 2. 校验验证码 String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginForm.getPhone()); String code1 = loginForm.getCode(); if (code == null || !code.equals(code1)) { // 3. session中没有验证码,或者验证码不正确,返回错误信息 return Result.fail("验证码错误!"); } // 4. 验证码正确,查询手机号用户 User user = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one(); // 5. 手机号用户不存在,注册 if (user == null) { user = createUserByPhone(loginForm.getPhone()); } // 6. 生成简单token String token = UUID.randomUUID().toString(true);//没有短横线的uuid // 7. 将用户转换成map保存到redis中 //将用户拷贝成dto对象,安全一点 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); //将对象转换成map类型,并把long类型id转换成字符串,方便存入redis Map<String, Object> usermap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() .ignoreNullValue().setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // 存入redis String tokenkey = LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenkey,usermap); // 8.设置有效期,因为hash不能存的时候设置有效期,所以只能后续设置 stringRedisTemplate.expire(tokenkey,LOGIN_USER_TTL,TimeUnit.MINUTES);//token在用户没有访问时,半个小时就过期 // 9. 返回token给前端,后续登录校验每次就看你有没有token return Result.ok(token); } // 根据手机号创建用户 private User createUserByPhone(String phone) { // 1. 创建用户,设置手机号、昵称 User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10)); // 2. 保存用户 save(user); return user; }
-
使用了两个拦截器分别做token的刷新和登录拦截
token刷新
// token刷新拦截器 public class RefreshInterceptor implements HandlerInterceptor { // 由于LoginInterceptor对象是手动创建,不受spring管理,不能直接注入,所以需要构造方法传入redis对象 private StringRedisTemplate stringRedisTemplate; public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } // 前置拦截(主要是校验登录状态) @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的token,根据token从redis中获取用户数据map String token = request.getHeader("authorization"); // token为空,无法查到用户,直接放行 if (StrUtil.isBlank(token)){ return true; } String tokenkey = LOGIN_USER_KEY + token; Map<Object, Object> usermap = stringRedisTemplate.opsForHash().entries(tokenkey); // 2. 如果map为空,无法查到用户,直接放行 if (usermap.isEmpty()){ return true; } // 3. 将map数据转换成userDTO类型 UserDTO userDTO = BeanUtil.fillBeanWithMap(usermap, new UserDTO(), false);//不忽略错误 // 4. 直接将用户保存到TreadLocal线程中 UserHolder.saveUser(userDTO); // 5. 用户一直在访问的话,刷新token过期时间,类似session的默认30分钟 stringRedisTemplate.expire(tokenkey,LOGIN_USER_TTL, TimeUnit.MINUTES);//刷新token过期时间 // 6. 放行 return true; } // 后置拦截(移除线程中的用户,防止内存泄露) @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
登录拦截
// 登录拦截器 public class LoginInterceptor implements HandlerInterceptor { // 前置拦截(主要是校验登录状态) @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //线程中没有用户,说明未登录,拦截 if (UserHolder.getUser() == null){ response.setStatus(401); return false; } //有用户,直接放行 return true; } // 后置拦截(移除线程中的用户,防止内存泄露) @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
商户查询缓存
1.什么是缓存
2.添加redis缓存
根据id查询商铺信息添加redis缓存
@Resource
private StringRedisTemplate stringRedisTemplate;
// 根据id查询商铺信息(添加缓存)
@Override
public Result selectShopById(Long id) {
// 1. 在redis中查询商铺(使用字符串的形式)
String key = CACHE_SHOP_KEY + id;
String shopjson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopjson)) {
// 2. 如果有,直接返回
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return Result.ok(shop);
}
// 3. 没有,去数据库查询
Shop shop = getById(id);
// 4. 数据库查询对象不存在,返回错误
if (shop == null) {
return Result.fail("商铺不存在!");
}
// 5. 数据库对象存在,加入缓存
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
// 6. 返回商铺对象
return Result.ok(shop);
}
店铺类型查询业务添加缓存
3.缓存更新策略
缓存更新的方式
选择主动更新好一些
主动更新我们需要考虑三个问题
先操作缓存还是先操作数据库
最佳实践方案
4.缓存穿透
缓存穿透产生的原因是什么?
- 用户请求的数据在缓存中和数据库中都不能存在,不断发起这样的请求,给数据库带来巨大压力。
缓存穿透的解决方案有哪些?
被动:
- 缓存空值
- 布隆过滤
主动:
- 增强id的复杂度,避免被猜测id规律
- 做好数据的校验格式
- 加强用户权限校验
- 做好热点参数的限流
具体代码实现(以查询商铺数据为例)
思路:缓存和数据库都未查询到数据,我们需要缓存一个空值,有效期2分钟,然后查询数据时,如果查询到的商铺数据为空值,说明该数据不存在,直接返回信息。
- 查询缓存时
//如果缓存数据是缓存击穿的结果,就返回不存在
//没有查询到商铺会返回Null,如果查到空值,说明结果不存在,是我们手动设置的
if (shopjson != null && shopjson.equals("")) {
return Result.fail("商铺不存在!");
}
- 查询数据库时
// 4. 数据库查询对象不存在,返回错误
if (shop == null) {
//缓存空值,防止缓存击穿,过期时间为两分钟
stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("商铺不存在!");
}
5.缓存雪崩
6.缓存击穿
什么是缓存击穿
高并发+重建缓存业务复杂
缓存击穿解决方案
具体代码实现
互斥锁:
获取锁是利用了redis的setnx命令设置值,即如果不存在该键,就设置值,如果存在了就设置失败。
// 使用互斥锁解决缓存击穿
private Result UseMutexesToResolveCacheBreakdowns(Long id) {
// 1. 在redis中查询商铺(使用字符串的形式)
String key = CACHE_SHOP_KEY + id;
String shopjson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopjson)) {
// 2. 如果有,直接返回
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return Result.ok(shop);
}
//如果缓存数据是缓存击穿的结果,就返回不存在
if (shopjson != null && shopjson.equals("")) {
return Result.fail("商铺不存在!");
}
// TODO 没有命中缓存,先获取锁
String lockkey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean issuccess = trylock(lockkey);
// TODO 获取失败,休眠然后重试
if (!issuccess) {
Thread.sleep(50);//休眠50毫秒
return UseMutexesToResolveCacheBreakdowns(id);//重试
}
// TODO 3.获取成功,查询数据库,重建缓存
shop = getById(id);
//由于缓存击穿出现的条件是高并发访问+缓存重建业务复杂,所以我们这里休眠一下,模拟业务复杂
//这样jmeter测试的时候就可以模拟了
Thread.sleep(200);
// 4. 数据库查询对象不存在,返回错误
if (shop == null) {
//缓存空值,防止缓存击穿,过期时间为两分钟
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("商铺不存在!");
}
// 5. 数据库对象存在,加入缓存,并设置超时剔除
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// TODO 缓存重建完成,释放锁
unlock(lockkey);
}
// 6. 返回商铺对象
return Result.ok(shop);
}
逻辑过期:
一般都会提前把数据导入缓存,因为是热点数据嘛。
// 使用逻辑过期解决缓存击穿
private Result UseLogicalExpirationToResolveCacheBreakdowns(Long id) {
// 1. 在redis中查询商铺(使用字符串的形式)
String key = CACHE_SHOP_KEY + id;
String shopjson = stringRedisTemplate.opsForValue().get(key);
// 2. 如果没有命中缓存,直接返回(按理说逻辑过期都会命中,这里判断是为了代码健壮性考虑)
if (StrUtil.isBlank(shopjson)) {
return null;
}
// 3.获取数据
// 这个地方json转成redisdata对象,获取data时需要强转为JSONObject,才能获取到data数据
RedisData redisData = JSONUtil.toBean(shopjson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//获取商铺对象
LocalDateTime expireTime = redisData.getExpireTime();//获取逻辑过期时间
// 4.缓存命中,需要判断缓存是否过期
if (expireTime.isAfter(LocalDateTime.now())){
// 4.1没有过期,返回缓存
return Result.ok(shop);
}
// 4.2如果过期,获取一个锁
String lockkey = LOCK_SHOP_KEY + id;
boolean issuccess = trylock(lockkey);
if (!issuccess) {
// 4.2.1获取失败,返回过期缓存
return Result.ok(shop);
}
// 4.2.2获取成功,新开一个线程重建缓存,重建之后释放锁
CACHE_REBUILD.submit(() ->{
try {
this.cacheRebuild(id,100L);//重建缓存,逻辑过期时间20秒
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockkey);//释放锁
}
});
// 5.获取锁成功后,当前线程就返回之前的缓存
return Result.ok(shop);
}
上面解决方案用到的方法
重建缓存,用了一个实体类封装数据和逻辑时间,这样比在实体类上添加字段好一点。
这里获取锁和释放锁的原理,利用了redis的setnx的特性,只能设置不存在的键,如果键存在就会设置失败,相当于每次只能设置一次。
//传入商铺id和逻辑过期时间重建缓存
public void cacheRebuild(Long id,Long expireTime){
// 1.查询商铺
Shop shop = getById(id);
// 模拟复杂业务
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 2.封装逻辑过期时间
RedisData<Shop> redisData = new RedisData<>();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));//添加逻辑过期秒
// 3.写入缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
//获取锁
public boolean trylock(String key) {
// 利用setnx只能设置一次,来达到锁的效果
Boolean islock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
// 不能直接返回islock,因为拆箱时有可能发生空指针我们这里使用工具类直接判断
return BooleanUtil.isTrue(islock);
}
//释放锁
public void unlock(String key) {
//直接删除setnx设置的锁
stringRedisTemplate.delete(key);
}
7.缓存工具封装
注意点:
- service层的返回值不要写成controller中的返回值Result(不规范),一般数据直接返回给controller层,然后controller层封装后返回Result对象。
- 使用泛型之前先定义泛型
- 缓存工具类,可以序列化任何java对象,封装了解决缓存穿透(缓存空值)和缓存击穿(逻辑过期)的解决方案。
//缓存工具类(避免解决缓存穿透和缓存击穿重复写代码)
@Slf4j
@Component
public class CacheClient {
//新建一个线程池,初始为10个线程
private static final ExecutorService CACHE_REBUILD = Executors.newFixedThreadPool(10);
private StringRedisTemplate stringRedisTemplate;//构造注入
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//1.将任意java对象序列化成json并存储在string类型的key中,并且可以设置TTL过期时间(普通redis存值并设置过期时间)
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
//2.将任意java对象序列化成json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题。
public void setWithLogical(String key, Object value, Long time, TimeUnit unit) {
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));//逻辑过期在redis层面不设置过期时间
}
//3.根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题。
public <R, ID> Result ResolveCacheBreakdowns(
String prefix, ID id, Class<R> clazz, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 在redis中查询商铺(使用字符串的形式)
String key = prefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
// 2. 如果有,直接返回
R r = JSONUtil.toBean(json, clazz);
return Result.ok(r);
}
//如果缓存数据是缓存穿透的结果,就返回不存在
if (json != null && json.equals("")) {
return Result.fail("数据不存在!");
}
// 3. 没有,去数据库查询
R r = dbFallback.apply(id);
// 4. 数据库查询对象不存在,返回错误
if (r == null) {
//缓存空值,防止缓存击穿,过期时间为两分钟
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("数据不存在!");
}
// 5. 数据库对象存在,加入缓存,并设置超时剔除
this.set(key, r, time, unit);
// 6. 返回商铺对象
return Result.ok(r);
}
//4.根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题。
// 使用逻辑过期解决缓存击穿(数据一开始会导入缓存,因为是热点数据)
public <R, ID> Result UseLogicalExpirationToResolveCacheBreakdowns(
String prefix, ID id, Class<R> clazz, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 在redis中查询商铺(使用字符串的形式)
String key = prefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 2. 如果没有命中缓存,直接返回(按理说逻辑过期都会命中,这里判断是为了代码健壮性考虑)
if (StrUtil.isBlank(json)) {
return null;
}
// 3.获取数据
// 这个地方json转成redisdata对象,获取data时需要强转为JSONObject,才能获取到data数据
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), clazz);//获取商铺对象
LocalDateTime expireTime = redisData.getExpireTime();//获取逻辑过期时间
// 4.缓存命中,需要判断缓存是否过期
if (expireTime.isAfter(LocalDateTime.now())) {//数据时间是未来时间,没有过期
// 4.1没有过期,返回缓存
return Result.ok(r);
}
// 4.2如果过期,获取一个锁
String lockkey = LOCK_SHOP_KEY + id;
boolean issuccess = trylock(lockkey);
if (!issuccess) {
// 4.2.1获取失败,返回过期缓存
return Result.ok(r);
}
// 4.2.2获取成功,新开一个线程重建缓存,重建之后释放锁
CACHE_REBUILD.submit(() -> {
try {
//1.从数据库查询数据
R r1 = dbFallback.apply(id);
//2.写入缓存
this.setWithLogical(key, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockkey);//释放锁
}
});
// 5.获取锁成功后,当前线程就返回之前的缓存
return Result.ok(r);
}
//获取锁
public boolean trylock(String key) {
// 利用setnx只能设置一次,来达到锁的效果
Boolean islock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
// 不能直接返回islock,因为拆箱时有可能发生空指针我们这里使用工具类直接判断
return BooleanUtil.isTrue(islock);
}
//释放锁
public void unlock(String key) {
//直接删除setnx设置的锁
stringRedisTemplate.delete(key);
}
}