IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 黑马redis实战-ThreadLocal -> 正文阅读

[大数据]黑马redis实战-ThreadLocal

问题:使用 Nginx 负载均衡时,用户的查询请求会分配到不同的JVM,当接收到用户请求时,JVM如何区分用户从而响应用户?

redis实现共享session登录

视频中采用的办法是:利用 redis 的 hash 结构,token 作为 key ,用户属性和属性状态分别作为 <field, value> 保存
在这里插入图片描述
然而:JVM 是如何区分用户的?比如:用户登录之后会进行抢购优惠券等活动,JVM在处理请求的时候,如何保证响应的是哪个用户呢?

1. token 与 userMap

UserServiceImpl.java

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 不一致,报错
            return Result.fail("验证码错误");
        }

        // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        // 5.判断用户是否存在
        if (user == null) {
            // 6.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }

上述登录流程 login(…) 中校验手机号验证码通过之后,使用 UUID 随机生成了 token,然后拼接上前缀之后,作为key保存到 redis 中

// 7.保存用户信息到 redis中
  // 7.1.随机生成token,作为登录令牌
   String token = UUID.randomUUID().toString(true);
   // 7.3.存储
   String tokenKey = LOGIN_USER_KEY + token;
   stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);

视频中采用的是验证码登录,每执行一次 login(…) 【点击 登录】都会执行生成验证码的逻辑。即无论是否是同一用户【手机号一致】,若是多次登录也会生成不同的token。

问题:同一个用户每次登录,有不同的token,对业务有影响吗?
测试结果:在 postman 中无论用哪个 token 抢购优惠券,都会使得该token对应的用户能够抢到优惠券。

问题分析:即使是不同的 token 里面存储的 userMap 都是一致的,在根据用户 id 抢购优惠券的时候,必然是同一个人在下单。
在这里插入图片描述

2. 登录手机号与userMap是怎么关联的?

2. 1 userMap 是怎么来的?

// 7.2.将User对象转为HashMap存储
   UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
   Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
           CopyOptions.create()
                   .setIgnoreNullValue(true)
                   .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

userMap,userDTO,user 三者之间的关系?
① userDTO 的作用:隐藏部分 user 属性
在这里插入图片描述
② userMap :userDTO 保存在 redis 中的形式
即:保存到 redis 中的 userMap 是经过了从 user —> userDTO —> userMap 的转化。

2.2 user 是怎么来的?

数据库 tb_user 中的字段信息
在这里插入图片描述

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        // 5.判断用户是否存在
        if (user == null) {
            // 6.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }
        // 8.返回token
        return Result.ok(token);
    }

① user 是通过使用 “phone” 查询数据库获取的【首次登录数据库中没有记录,会新建用户并保存到数据库中】

② “phone” 是通过 Login(LoginFromDTO loginFrom) 传输的参数对象 loginFrom 获取的

2.3 loginFrom 怎么来的?

UserController.java

@PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        return userService.login(loginForm, session);
    }

定位到Controller 层,涉及到与视图层的交互.

2.3.1 如何确定是前端哪个页面发送的请求呢?

由于对 SpringMVC 的知识点不了解,做以下测试进行分析
① 点击 “发送验证码”
在这里插入图片描述
UserController.java

@RestController
@RequestMapping("/user")
public class UserController {
      ...
    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }

根据请求中的 url :/user/code 定位到 UserController 中的 sendCode(…)

推测:点击 “登录” 会跳转到 login() 对应的页面中

② 点击 “登录”
在这里插入图片描述
在vscode上打开前端的代码,搜索 “请先确认阅读用户协议!”

定位到前端的 login.html

methods: {
      login(){
        if(!this.radio){
          this.$message.error("请先确认阅读用户协议!");
          return
        }
        ...

③ 点击 “阅读协议”
在这里插入图片描述
前端弹出"手机号和验证码不能为空"

问题:前端是怎么获得用户输入的呢?

const app = new Vue({
    el: "#app",
    data: {
      radio: "",
      disabled: false, // 发送短信按钮
      codeBtnMsg: "发送验证码", // 发送短信按钮提示
      form:{
      }
    },
    methods: {
      login(){
         .....
        if(!this.form.phone || !this.form.code){
          this.$message.error("手机号和验证码不能为空!");
          return
        }
        axios.post("/user/login", this.form)
        ....
      },

可以看到提示 “手机号和验证码不能为空!”,是在执行 if(!this.form.phone || !this.form.code)之后,也即是从 from 中获取的 phone 和 code

④ from 怎么保存的 phone 和 code ?
在这里插入图片描述
而之后执行了 axios.post("/user/login", this.form),会将 from 作为请求参数

发送 login 请求时,将 form{phoone,code} 作为负载携带到请求里面
在这里插入图片描述
再回头看 UserController.java 中 login(…) 的逻辑,将 from 中的内容通过 @Requsetbody 读取到 LoginFromDTO 对象中

@PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        return userService.login(loginForm, session);
    }
@Data
public class LoginFormDTO {
    private String phone;
    private String code;
}

到此为止:可以就 登录手机号与userMap 是怎么关联的给出答案

① 前端页面填写 phone,code 时会把信息保存到 from 表单,后者在 axios.post异步请求时将 phone 字段附加到 url 中

② 后端的 Controller 层,将请求分发给 login(…) 利用 @RequestBody获取 form{phone,code}

③ login(…) 登录流程中,通过loginFromDTO 获取 phone,通过 phone 查询数据库中的 user

④ 将 user 转化为存储在 redis 中的 userMap

3. 校验验证码之后呢?

① 验证码—登录凭证
在之前的处理流程中,登录成功后会跳转到首页。该流程仅仅通过校验手机号是否一致,判断能否跳转,并没有关于用户身份识别处理。
在这里插入图片描述
返回给前端 token 的逻辑是:

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        return Result.ok(token);
    }

② 多出来的 authorization 字段

上述:修改前端代码,禁止页面跳转,主要是为了验证,验证码一致就能登录
这时,再点击其他页面也是可以的【包括输入相同手机号进入到已注册用户的界面】,只是多了字段 authorization
在这里插入图片描述
点击其他页面的时候,请求头里面多了 authorization 字段,该字段的内容正是后端传给前端的 token

搜索 authorization 定位到 前端 的 commo.js 文件
在这里插入图片描述
测试其他页面,发现都已经携带了 authorization 字段,【/user/code 界面,以及 user/login 界面是没有的,因为还没有请求 token】

4. authorizaion 与 拦截器

通过上述分析已经知道:用户登录后,用户跳转任意页面时发送的HTTP请求头中都会携带 authorization 字段,而 authorization 字段里面的值正是保存在 redis 的hash 结构中,用于获取userMap 的key

通过 authorization --> token --> userMap 的处理逻辑是写在 拦截器中

RefreshTokenInterceptor.java

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 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;
    }

写在拦截器中的目的:每个页面都需要获取用户进行相关操作,而拦截器可以在请求分发前先执行

上面逻辑处理很重要的一点是:将获取到的用户信息保存在了 threadLocal 中,这也是JVM线程区分用户的关键

MvcConfig.java

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }

RefreshTokenInterceptor 会拦截所有请求路径,然后执行上面,获取token,根据 token 从 redis 中查询用户,然后将用户信息保存到 threadLocal 中

5. threadLocal 问题的关键所在

上面已经分析到:跳转不同的页面,都会先根据 authorization 获取 token,进而将 userMap 保存到 threadLocal ,则之后的处理逻辑都可以通过 threadLocal 获取 userMap 中的信息,即获取 user_id

下面以秒杀抢购为例验证:

@Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        long orderId = redisIdWorker.nextId("order");
        // 1.执行 lua 脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString(),
                String.valueOf(orderId)
        );
        int r = result.intValue();
        // 2.判断结果是否为 0
        if(r != 0){
            // 2.1不为0,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }

        return Result.ok(orderId);
    }

可以看到:执行秒杀逻辑前,是先从 userHolder 中获取 userId

而 UserHolder 是借助 ThreadLocal 属性将 UserDTO 保存到 ThreadLocalMap 中

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();
    }
}

6. ThreadLocal 与 ThreadLocalMap

6.1 使用 ThreadLocal 的原因

问题:为什么要把 userDTO 保存到 ThreadLocalMap 中,直接根据token 获取到的去 redis 中查不就行了吗?

分析:通过 token 查询 redis 中的 userDTO 是写在 拦截器中的。
写在拦截器中的好处是:查询 userDTO 的逻辑可以只写一遍,每个请求执行前先经过拦截器去查询即可

写在拦截器中的缺点是:查询 userDTO 的逻辑需要与请求解耦,任何请求来都可以查询,所以就需要使用一种结构,可以将请求的线程和查询的结果绑定。而这就是使用 ThreadLocal 的原因

6.2 ThreadLocal 与 Thread.currentThread()

问题:ThreadLocal 是如何将用户线程与请求结果进行绑定的?

① 首先以一个测试用例来分析
Threadlocal 原理

public class ThreadLocalTest02 {
    public static void main(String[] args) {

        ThreadLocal<String> local = new ThreadLocal<>();

        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
            local.set(Thread.currentThread().getName() + ":" + i);
            System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + local.get());
        }).start());
    }
}

代码逻辑:

  • main 主线程中创建 ThreadLocal 对象
  • main 主线程中开启 10 个子线程
  • 使用 local.set(…) 保存 Thread-i:i
  • 获取当前线程 与 local 中保存的结果

运行结果:

线程:Thread-0,local: Thread-0:0
线程:Thread-3,local: Thread-3:3
线程:Thread-1,local: Thread-1:1
线程:Thread-2,local: Thread-2:2
线程:Thread-5,local: Thread-5:5
线程:Thread-4,local: Thread-4:4
线程:Thread-6,local: Thread-6:6
线程:Thread-7,local: Thread-7:7
线程:Thread-8,local: Thread-8:8
线程:Thread-9,local: Thread-9:9

上述结果一个很重要的特点是:获取的 Thread 线程,与从 threadLocal 中取到的线程是一致的

这是为什么?从 local.get() 入手分析:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  • 先通过 Thread.currentThread() 获取当前线程
  • 根据 当先线程获取 ThreadLocalMap 对象
  • 从 threadLocalMap 中获取 value

问题:map.getEntry(this) 这里的this 指的是什么?

this 指调用该方法的对象,local.get() ,这里的 this 指的是 local 对象

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看到 Entry 中确实存的key 是 ThreadLocal 类型的

问题:既然分析到 ThreadLoalMap 的 Entry 中存储的key 是 threadlocal,那对于本例来说,10个子线程的 key 不都是 local吗?为什么没有出现 不同线程由于key 相同,value 被覆盖的情况呢?

6.3 ThreadLocal && ThredLocalMap && threadLocals

为了回答上面 key 相同,value 没有被覆盖的问题,需要从 local.set(…) 入手

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

当前线程获取 ThreadLocalMap 是通过 getMap(t)

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

返回了 t.threadLocals,注意这里居然是返回了线程的属性

ThreadLocal.ThreadLocalMap threadLocals = null;

看到这里,ThreadLocal,ThreadLocalMap,threadLocals 三者的关系一目了然

  • ThreadLocalMap 是 ThreadLocal 的静态内部类
  • threadLocals 是Thread中的属性,是ThreadLocal.ThreadLocalMap 类型的

其实到这里,也就解释了为什么,key = local 的时候,10个子线程不会出现value 覆盖的情况

  • <key. value> 是以Entry 的形式保存在 ThreadLocalMap 中的
  • 每次添加和获取元素前都要获取 map,此处的map 是 Thread 中的 threadlocals 属性,是属于线程的

即:开启了10个子线程,创建了 1 个 ThreadLocal 对象作为key,但线程的值是保存在线程中的 ThreadLocal.ThreadLocalMap 类型的属性 threadLocals 中的
在这里插入图片描述

6.4 为什么不直接用 ThreadLocalMap

上面已经分析到:数据实际上是存储在 ThreadLocalMap 中的,那为什么不直接用 ThreadLocalMap ,而需要借助 ThreadLocal 呢?

考虑将 Thread 中的 threadLocals 改写下会怎样

ThreadLocalMap threadLocals = null;
  • Thread 类中有 ThreadLocalMap 类型的属性 threadLocals 用于保存线程独立变量,key如何设计?
  • threadLocals 是默认权限的,该属性可以被同一包下的类访问

即使下面一条将 default 改为 私有权限的,由于Thread 无法做到将类修饰为 final 类型的,子类完全可以放宽 threadLocals 的访问权限,这就造成了 threadLocals 不可能做到像 局部变量一样的线程安全的了

还有,不用 ThreadLoal 如何设计 key 呢?

使用 ThreadLocalMap 目的是为了保存线程变量,使该线程私有?则必然有通过线程获取value 的需求,那么key 就必然与 Thread.currentThread() 有关,这就回到了当不止有一个value 的时候,key又都需要与 Thread.currentThread() 有关,如何确保不会因为 key 一致,导致 value 被覆盖的问题上

下面看看使用 ThreadLocal 的好处

  • 由于 ThreadLocalMap 是 ThreadLocal 中的静态内部类,所以必须要通过 ThreadLocal 访问
  • 由于是 Default 访问权限的,意味着在 java.lang 包外,无法通过 thread 对象获取 threadLocals
  • ThreaLocal 作为key,当获取线程变量的时候,都会先获取到 Thread.currentThread(),进而才能获取到 threadlocals

总结来说:
ThreadLocal 既可以保证线程拥有私有变量不受其他线程影响,又可以解决线程和线程变量绑定问题

回到最初的问题:JVM 是如何识别用户的?
jvm 线程根据 token 获取到 redis 中的 user 信息后,将其保存在自己的 threadlocals 中,响应客户端请求时都会先从 threadlocals中取值

每个线程中的 threadlocals 是互不影响的,只能由对应的线程获取其 threadlocals 中保存的用户信息

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:28:13  更:2022-04-09 18:28:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:04:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码