黑马redis实战-ThreadLocal_执霜的博客-程序员秘密_threadlocal和redis

技术标签: java  项目  

问题:使用 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 中保存的用户信息

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43156556/article/details/124028925

智能推荐

Android集成Zxing实现二维码的扫描与生成(含动态权限)_李诗雨的博客-程序员资料

不诗意的女程序猿不是好厨师~ 【转载请注明出处:From李诗雨】目前在做一个和机顶盒相关的app,项目中需要使用二维码扫描来绑定机顶盒。由于工期的原因,也没时间细细研究,只是粗略集成了一下。在此,又重新写了一个demo,以示记录。源码demo:点击下载。Demo说明:①实现二维码的扫描②实现二维码的生成③使用RxPermissions做了动态

STM32 串口通信介绍及cubemx配置_Nie_Hen的博客-程序员资料

学习理解STM32的串口通信,中断以及I2C的使用。应用:使用中断和串口通通信获取按键值发送出来并显示到数码管上。使用I2C 将获取到的按键值保存到内存中。串口通信计算机的CPU与外部设备之间的信息交换,以及计算机与计算机之间的信息交换过程称为通信。并行通信数据字节的各位同时传送的通信方式。并行通信的优点是数据传送速度快,缺点是占用的传输线条数多,适用于近距离通信。串行通信(Se...

android 背景图缩放,解决android:background背景图片被拉伸问题_weixin_39639600的博客-程序员资料

ImageView中XML属性src和background的区别:background会根据ImageView组件给定的长宽进行拉伸,而src就存放的是原图的大小,不会进行拉伸。src是图片内容(前景),bg是背景,可以同时使用。此外:scaleType只对src起作用;bg可设置透明度,比如在ImageButton中就可以用android:scaleType控制图片的缩放方式如上所述,backg...

qt用odbc连接mysql_【原创】Qt 使用ODBC driver 连接SQL Server_午餐时间到了的博客-程序员资料

最近在做数据库的课程设计。第一个需要解决的问题是使用什么工具来实现这个系统。经过一番资料查找,决定使用SQL Server Express 2012作为服务器,使用Qt作为编写客户端程序语言。问题是client如何连接SQL Server? 下面是我的解决方法。1.开启windows上的SQL Server 的ODBC驱动ODBC 是一个调用级接口,它使得应用程序得以访问任何具有 ODBC 驱动程...

翻页时钟代码大公开_lovefan的博客-程序员资料_翻页钟开源代码

不少朋友向我要翻页时钟的代码,现在贴给大家。代码水平有限,见谅。看不明白的可以问我:)js// miniprogram/pages/flipClock/jsconst moment = require('../../../utils/moment-with-locales.min.js');const Lunar = require('../../../utils/lunar.js');var startX, endX;var moveFlag = true; // 判断执行滑动事件.

JAVA切换不了FTP服务器目录_解决linux下ftp指定访问目录无法修改的问题_豪睿刘爱上楼楼梯的博客-程序员资料

他的系统是CentOS,是RH派系的。我把vsftpd安装配置好了,以为大功告成,但客户端访问提示如下错误:500 OOPS: cannot change directory:/home/ftp原因是他的CentOS系统安装了SELinux,因为默认下是没有开启FTP的支持,所以访问时都被阻止了。//查看SELinux设置# getsebool -a|grep ftpftpd_disable_tr...

随便推点

centos7搭建zabbix4.4_萌新包大人的博客-程序员资料

centos7搭建zabbix4.4环境准备:1、centos7服务器一台(版本CentOS-7-x86_64-Minimal-1908)2、已分配主机IP地址172.18.100.113搭建步骤:1、更改主机名、修改hosts文件HOSTNAME=zabbix hostnamectl set-hostname "$HOSTNAME" echo "$HOSTNAME"&gt;/etc/hostname echo "$(grep -E '127|::1' /etc/hosts)"&gt;/e

thttpd+cgilua_简单并快乐着的博客-程序员资料

PC安装1: 下载thttpd:  http://acme.com/software/thttpd/2: 参考前面文章安装kepler/xavante (实际上要不了那么多lua module,但这种方式安装最简单),或者只安装如下module:wsapi  cgilua  md5  luasocket (luaexpat)3:  创建www组groupadd ww

阿里的BUG智能定位神器!直接定位线上BUG,超给力!必须收藏!_l_瓶中精灵的博客-程序员资料

点击上方“阿拉奇学Java”,选择“置顶或者星标”每天早晨00点00分, 与你相约!往日回顾:Java 14 令人期待的 5 大新特性,打包工具终于要来了! 定位过程分析代码...

机器学习中用到的概率知识_机器学习必备的概率统计基础_weixin_40009393的博客-程序员资料

现如今,计算机科学、人工智能、数据科学已成为技术发展的主要推动力。无论是要翻阅这些领域的文章,还是要参与相关任务,你马上就会遇到一些拦路虎:想过滤垃圾邮件,不具备概率论中的贝叶斯思维恐怕不行;想试着进行一段语音识别,则必须要理解随机过程中的隐马尔科夫模型;想通过观察到的样本推断出某类对象的总体特征,估计理论和大数定理的思想必须建立;在统计推断过程中,要理解广泛采用的近似采样方法,蒙特卡洛方法以及马...

oracle使用sqlplus远程连接ASM实例的方式_kadwf123的博客-程序员资料_oracle连接asm实例

sqlplus sys/[email protected]:1521/+ASM as sysasm至于配置成tns,照着普通oracle连接方式配置就行,此处每次不记得命令格式,写在这边记录一下;最后的+ASM指的是服务名,不是实例名,写成实例名连不上。...

一文了解人脸识别:从实现方法到应用场景都讲明白了_大数据v的博客-程序员资料

导读:在本文中,我们将会接触到一个既熟悉又陌生的概念——人脸识别。之所以熟悉,是因为人脸识别技术在我们日常生活中应用极其广泛,例如火车站刷脸验票进站、手机人脸解锁等;之所...