短信登录功能
发送短信验证码实现流程
- 提交手机号
- 校验手机号
- 生成验证码,并保存
- 保存验证码到redis
- 发送验证码
@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();
}
短信验证码登录注册实现流程
- 提交手机号和验证码
- 验证验证码
- 根据手机号查询用户
- 用户是否存在(如果不存在创建新用户并保存用户到数据库)
- 保存用户到redis
@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<String, Object> 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);
}
校验登录状态实现流程
- 请求并携带cookie
- 从redis获取用户
- 判断用户是否存在(如果不存在则拦截)
- 如果存在则保存用户到threadlocal
@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<Object, Object> userMap =
stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY + token);
//用户不存在
if (userMap.isEmpty()) {
return true;
}
//hash转UserDTO存入ThreadLocal
UserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false));
//token续命
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
缓存功能
缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,一般读写性能较高。
作用:
- 降低后端负载。
- 提高读写效率,降低响应时间。(解决高并发问题)
成本:
- 数据一致性成本
- 代码维护成本(缓存击穿)
- 运维成本
缓存更新策略
内存淘汰:利用redis的内存淘汰机制,内存不足时自动淘汰部分数据。
超时剔除:添加ttl时间,到期自动删除缓存(兜底)。
主动更新:在修改数据库的同时,更新缓存。
- 方案1:由缓存的调用者,在更新数据库的同时更新缓存。
- 方案2:缓存与数据库整合为一个服务,由服务来维护一致性。
- 方案3:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致。
如何保证缓存与数据库的操作的同时成功或失败(原子操作问题)?
- 单体系统,将缓存与数据库操作放在一个事务。
- 分布式系统,利用tcc等分布式事务方案。
先操作缓存还是先操作数据库(线程安全)?
先操作数据库再删除缓存的操作更保险,因为数据库的操作时间一般要比缓存慢。
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会达到数据库。
缓存空对象(被动方案)
优点:实现简单,维护方便
缺点:额外的内存消耗(缓存垃圾,设置ttl删除)、可能造成短期的不一致(插入时更新缓存)
布隆过滤(被动方案)
在客户端和redis之间加入布隆过滤器,如果不存在则拒绝,存在则放行。(用byte数组存储,hashcode二进制hash对应位置,若为1存在,若为0不存在)
优点:内存占用少,没有多余key
缺点:实现复杂,存在误判可能
主动方案
- 增加id的复杂度,避免被猜测id规律(雪花算法)
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
/**
* 缓存穿透
* @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的ttl添加随机值(同一时间段大量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);
}