本文最后更新于85 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com
学了几个项目,这些东西都是随便看了看就过过去了,没有系统了解其原理,感觉很模式,今天就来学习一下
Session登录

发送验证码
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// TODO 发送短信验证码并保存验证码
return Result.fail("功能未完成");
}
服务层
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
//校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
//保存验证码到session
String code = RandomUtil.randomNumbers(6);
session.setAttribute("code", code);
//发送验证码
log.debug("发送验证码成功,验证码:{}", code);
//返回ok
return Result.ok();
}
用户登录
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//校验手机号
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
return Result.fail("手机号格式错误");
}
//校验验证码
String code = (String) session.getAttribute("code");
//不一致报错
if (code == null || !code.equals(loginForm.getCode())) {
return Result.fail("验证码错误");
}
//一致,根据手机号查询用户
User user = query().eq("phone", loginForm.getPhone()).one();
//判断用户是否存在
if (user == null) {
//不存在,创建新用户并保存
user = createUserWithPhone(loginForm.getPhone());
save(user);
log.debug("创建用户成功:{}", user);
}
//保存用户信息到session
session.setAttribute("user", user);
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
return user;
}
这些完成后还要有登录凭证,登录校验拦截器
ThreadLocal拦截
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取session
HttpSession session = request.getSession();
//获取session中的用户
Object user = session.getAttribute("user");
//判断用户是否存在
if (user == null){
//不存在,拦截,返回401
response.setStatus(401);
return false;
}
//存在,保存用户信息到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
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();
}
}
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
集群session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失问题。
多个TOMcat服务器不会共享session


//保存验证码到session
String code = RandomUtil.randomNumbers(6);
//session.setAttribute("code", code);
//保存验证码到redis,并设置过期时间
stringRedisTemplate.opsForValue().set("login:code:" + phone, code,2, TimeUnit.MINUTES);
//保存用户信息到session
UserDTO userDTO = new UserDTO();
BeanUtil.copyProperties(user, userDTO);
//session.setAttribute("user",userDTO);
//把用户存到redis
//生成随机token
String token = UUID.randomUUID().toString(true);
//将userDTIO转换为HashMap
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
//添加到redis
stringRedisTemplate.opsForHash().putAll("login:token:"+token, userMap);
//设置token有效期
stringRedisTemplate.expire("login:token:"+token,LOGIN_CODE_TTL,TimeUnit.MINUTES);
拦截器token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
response.setStatus(401);
return false;
}
//获取token中的用户
Map<Object, Object> UserDtoMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
UserDTO user = BeanUtil.fillBeanWithMap(UserDtoMap, new UserDTO(), false);
确保都是String类型的
//将userDTIO转换为HashMap,Long类型要转换为String类型
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).
setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
只拦截了登录等请求,其他请求无法刷新有效期,新定义一个拦截器

if (StrUtil.isNotBlank(shopJson)) {
//存在,返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//命中了空值缓存,返回错误信息
if (shopJson != null) {
return Result.fail("商户不存在");
}
缓存穿透,传递空值要这样判断.
逻辑过期

private Shop queryWithLogicalExpire(Long id) {
//从redis从查询商户
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if (StrUtil.isBlank(shopJson)) {
//不存在,返回null
return null;
}
//命中,需要判断过期时间
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否过期
//未过期,直接返回商户信息
if (expireTime.isAfter(LocalDateTime.now())){
return shop;
}
//已过期,缓存重建
//获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLocked = tryLock(lockKey);
//判断是否获取锁成功
if (isLocked){
//成功,获取锁成功,开启独立线程,实现缓存重建,根据id查询数据库
CACH_REBUILD_EXECUTOR.submit(() -> {
try {
saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
unLock(lockKey);
}
});
}
//返回
return shop;
}
设置一个逻辑过期类
@Data
public class RedisData {
// 过期时间
private LocalDateTime expireTime;
private Object data;
}
private void saveShop2Redis(Long id, Long expireSeconds){
//查询店铺数据
Shop shop =getById(id);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
乐观锁与悲观锁




Redission可重入锁










