发送短信验证码实现流程
@Override
public Result sendCode(String phone) {
//校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail(UserErrorConstant.PHONE_FORMAT_ERROR);
}
//生成验证码
String code = RandomUtil.randomNumbers(6);
//保存验证码(有效期:5min)
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
return Result.ok();
}
短信验证码登录注册实现流程
@Override
public Result loginByCode(LoginFormDTO loginFormDTO) {
//校验手机号
String phone = loginFormDTO.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail(UserErrorConstant.PHONE_FORMAT_ERROR);
}
//从redis获取验证码并验证
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+phone);
String code = loginFormDTO.getCode();
if(cacheCode==null||!cacheCode.equals(code)){
return Result.fail(UserErrorConstant.VERIFICATION_CODE_ERROR);
}
//根据手机号查询用户
User user = query().eq("mobile",phone).one();
//判断用户是否存在
if(user==null){
user = createUserWithPhone(phone);
}
//使用jwt令牌
String token = UUID.randomUUID().toString();
//将user对象转成HashMap存储(由于使用的是stringRedisTemplate,所以要把id转为string)
UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
userDTO.setToken(token);
Map userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue == null ? "" : fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY+token,userMap);
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL,TimeUnit.HOURS);
//返回token
return Result.ok(userDTO);
}
校验登录状态实现流程
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从请求头中获取token
String token = request.getHeader("authorization");
if (StringUtils.isEmpty(token)) {
//不存在token
return true;
}
//从redis中获取用户
Map
缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,一般读写性能较高。
作用:
成本:
内存淘汰:利用redis的内存淘汰机制,内存不足时自动淘汰部分数据。
超时剔除:添加ttl时间,到期自动删除缓存(兜底)。
主动更新:在修改数据库的同时,更新缓存。
先操作数据库再删除缓存的操作更保险,因为数据库的操作时间一般要比缓存慢。
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会达到数据库。
优点:实现简单,维护方便
缺点:额外的内存消耗(缓存垃圾,设置ttl删除)、可能造成短期的不一致(插入时更新缓存)
在客户端和redis之间加入布隆过滤器,如果不存在则拒绝,存在则放行。(用byte数组存储,hashcode二进制hash对应位置,若为1存在,若为0不存在)
优点:内存占用少,没有多余key
缺点:实现复杂,存在误判可能
/**
* 缓存穿透
* @param id
* @return
*/
public Result queryWithPassThrough(int id){
String key = RedisConstants.CACHE_SHOP_KEY+id;
//从redis查询企业用户信息缓存
String userJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isNotBlank(userJson)){
User user = JSONUtil.toBean(userJson,User.class);
return Result.ok(user);
}
//判断命中的是否是空值
if(userJson!=null){
//返回错误信息
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//不存在,根据id查询数据库
User user = userMapper.queryUserById(id);
//不存在,返回错误
if(user==null){
//缓存穿透-》将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//存储到redis中,设置了超时时间
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
return Result.ok(user);
}
缓存雪崩是指同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。
缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。(无数请求缓存重建)
未命中需要获取锁才能查询数据库重建缓存数据,写入缓存后再释放锁。
优点:没有额外内存消耗,保证数据的一致性,实现简单。
缺点:线程需要等待,性能受影响,可能有死锁风险。
/**
* 缓存击穿->互斥锁
* @param id
* @return
*/
public Result queryWithPassMutex(int id) {
String key = RedisConstants.CACHE_SHOP_KEY+id;
//从redis查询企业用户信息缓存
String userJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isNotBlank(userJson)){
User user = JSONUtil.toBean(userJson,User.class);
return Result.ok(user);
}
//判断命中的是否是空值
if(userJson!=null){
//返回错误信息
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//缓存重建
String lockKey = RedisConstants.LOCK_SHOP_KEY+id;
User user =null;
try {
boolean isLock = tryLock(lockKey);
if(!isLock){
Thread.sleep(50);
//风险!改成while(true)轮询就好
return queryWithPassMutex(id);
}
//不存在,根据id查询数据库
user = userMapper.queryUserById(id);
//不存在,返回错误
if(user==null){
//缓存穿透-》将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//存储到redis中,设置了超时时间
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
} catch (InterruptedException e){
throw new RuntimeException(e);
} finally {
//释放互斥锁
unlock(lockKey);
}
return Result.ok(user);
}
发现逻辑时间已过期,未命中获取互斥锁,返回过期数据,开启新线程去做缓存重建并释放锁,如果有新线程来的时候还是会返回新数据。
优点:线程无需等待,性能较好。
缺点:不保证一致性,有额外内存消耗,实现复杂。
/**
* 缓存穿透
* @param id
* @return
*/
public Result queryWithPassThrough(int id){
String key = RedisConstants.CACHE_SHOP_KEY+id;
//从redis查询企业用户信息缓存
String userJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isNotBlank(userJson)){
User user = JSONUtil.toBean(userJson,User.class);
return Result.ok(user);
}
//判断命中的是否是空值
if(userJson!=null){
//返回错误信息
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//不存在,根据id查询数据库
User user = userMapper.queryUserById(id);
//不存在,返回错误
if(user==null){
//缓存穿透-》将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//存储到redis中,设置了超时时间
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
return Result.ok(user);
}