腾讯面试:SaaS多租户,如何设计?

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业网易、美团、字节、如阿里、滴滴、极兔、有赞、希音、百度、美团的面试资格,遇到很多很重要的面试题:

多租户设计,如何 技术选型?

什么是多租户,如何做架构设计?

你们的多租户,是怎么隔离的?

最近有小伙伴在面试腾讯的企业BG,又遇到了相关的面试题。小伙伴懵了, 当然,面试也就挂了。

小伙伴赶紧来求助尼恩,尼恩借助这个小伙伴的面试真题,给大家做一下系统化、体系化的微服务底层架构 梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V149版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

特别提示:尼恩的3高架构宇宙,持续升级。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】取

文章目录

    • 尼恩说在前面
    • 一、SaaS多租户简介
      • 什么是SaaS多租户技术
      • 1.1、什么是 SaaS多租户
      • 1.2、SaaS多租户的优势
      • 1.3、多租户模型
    • 二、SaaS多租户的数据隔离设计方案
      • 2.1、三种数据隔离架构设计的对比如下:
      • 2.2、MyBatis-Plus多租户插件优雅实现数据隔离
    • 三、MyBatisPlus实现多租户功能
      • 3.1、表及实体类添加租户ID
      • 3.2、application文件中添加多租户配置和新增配置属性类
      • 3.3、编写多租户处理器实现TenantLineHandler接口
      • 3.4、MybatisPlus配置类启用多租户拦截插件
        • 运行sql实例:
      • 3.5、特定SQL语句忽略拦截
    • 参考文献:
    • 说在最后:有问题可以找老架构取经
    • 尼恩技术圣经系列PDF

一、SaaS多租户简介

多租户技术是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。它是为共用的数据中心内如何以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍可保障客户的数据隔离。简单来说是一个单独的实例可以为多个组织服务。

多租户是SaaS(Software-as-a-Service)下的一个概念,意思为软件即服务,即通过网络提供软件服务。SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作的实际需求,通过互联网向厂商租用所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得SaaS平台供应商提供的服务。

SaaS服务尤其利于一些中小企业,以低成本实现自己的软件需求。

注意:请点击图像以查看清晰的视图!

什么是SaaS多租户技术

  • 多租户技术或称多重租赁技术,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业)共用相同的系统或程序组件,并且确保各用户间数据隔离性。
  • 在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。多租户的重点就是同程序下实现多用户数据的隔离。

1.1、什么是 SaaS多租户

  • SaaS,是Software-as-a-Service的缩写名称,意思为软件即服务,即通过网络提供软件服务。
  • SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得Saas平台供应商提供的服务。
  • SaaS服务通常基于一套标准软件系统为成百上千的不同客户(又称为租户)提供服务。这要求SaaS服务能够支持不同租户之间数据和配置的隔离,从而保证每个租户数据的安全与隐私,以及用户对诸如界面、业务逻辑、数据结构等的个性化需求。由于SaaS同时支持多个租户,每个租户又有很多用户,这对支撑软件的基础设施平台的性能、稳定性和扩展性提出很大挑战。

多租户是SaaS领域的特有产物,探究何为多租户需回归到对SaaS的理解上。

SaaS服务是指部署在云上的,客户可以按需购买,并通过网络请求就能获取到的服务;也就是说,在这样的场景下,会有N个客户同时使用同一套SaaS服务。

那么对SaaS服务供应商来说,构建SaaS体系需要完成两部分工作:上层服务+底层多租户系统。

上层服务是供应商对外售卖的软件服务,其可以为客户创造价值、为公司带来营收;而底层多租户系统则是SaaS模式实现的具体方式,公司在对外售卖SaaS服务时,需要考虑如何实现客户之间的数据隔离、服务的权限控制、计费管理等;因此需要引入多租户概念来解决上述问题。

通过多租户系统,公司可以更好的管理客户和上层服务,客户也可以更好的使用软件服务。这也就是多租户系统存在的意义了。

1.2、SaaS多租户的优势

  • 开发和运维成本低
  • 按需付费,节约成本
  • 即租即用,软件版本更新快
  • 故障排查更及时
  • 大数据和AI的能力支持更强大

1.3、多租户模型

注意:请点击图像以查看清晰的视图!

如图所示,涉及主要模型有以下几类:

(1)租户:指一个企业客户或是个人客户,租户之间数据与行为隔离,上下级租户间通过授权实现数据共享。每个租户只能操作归属或授权给该租户的数据;

(2)组织:如果租户是一个企业客户,通常就会拥有自己的组织架构;

(3)用户:租户下的具体使用者,拥有用户名、密码、邮箱等账号信息的自然人;

(4)角色:用户操作权限的集合;

(5)员工:组织内的某位员工;

(6)解决方案:为了解决客户的某类型业务问题,SaaS供应商一般都将产品和服务组合在一起,为客户提供整体的打包方案;

(7)产品能力:能够帮助客户实现场景解决方案闭环的能力;

(8)资源域:用来运行1个或多个产品应用的一套云资源环境;

(9)云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。

二、SaaS多租户的数据隔离设计方案

多租户对于用户来说,最主要的一点就在于数据隔离。

绝对不能出现:一个用户登了A用户单位的号,但是看到了B用户单位的数据。因此,多租户的数据库设计方案和代码实现就相当有必要考虑了。

目前开发者们普遍接受的SaaS多租户设计方案,常见的大概就3种:即为每个租户提供独立的数据库、独立的表空间、按字段区分租户,每种方案都有其各自的适用情况。

  • 一个租户独立一个数据库

一个租户独立使用一个数据库,那就意味着我们的SaaS系统需要连接多个数据库,这种实现方案其实就和分库分表架构设计是一样的,好处就是数据隔离级别高、安全性好,毕竟一个租户单用一个数据库,但是物理硬件成本,维护成本也变高了。

  • 独立的表空间

这种方案的实现方式,就是所有租户共用一个数据库系统,但是每个租户在数据库系统中拥有一个独立的表空间。

  • 按租户id字段隔离租户

这种方案是多租户方案中最简单的数据隔离方法,即在每张表中都添加一个用于区分租户的字段(如tenant_id或org_id啥的)来标识每条数据属于哪个租户,当进行查询的时候每条语句都要添加该字段作为过滤条件,其特点是所有租户的数据全都存放在同一个表中,数据的隔离性是最低的,完全是通过字段来区分的,很容易把数据搞串或者误操作。

2.1、三种数据隔离架构设计的对比如下:

隔离方案成本支持租户数量优点缺点
独立数据库系统数据隔离级别高,安全性,可以针对单个租户开发个性化需求数据库独立安装,物理成本和维护成本都比较高
独立的表空间较多提供了一定程度的逻辑数据隔离,一个数据库系统可支持多个租户数据库管理比较困难,表繁多,同时数据修复稍复杂
按租户id字段区分维护和购置成本最低,每个数据库能够支持的租户数量最多隔离级别最低,安全性也最低

大部分公司都是采用第三种多租户设计方案:按租户id字段隔离租户 架构设计实现多租户数据隔离的。

因为这种方案服务器成本最低,但是提高了开发成本。

2.2、MyBatis-Plus多租户插件优雅实现数据隔离

该系统只有一个数据库,所有租户共用数据表。

在每一个数据表中增加一列租户ID,用以区分租户的数据。

增删查改时,一定要带上租户ID,否则就会操作到其他租户的数据。因此,这里的设计一定要重点考虑。

我们要保证的就是一定不要忘记带上租户ID。一个很好的方案就是通过AOP的方案,隐式的为我们的每一个SQL带上这个租户ID。

推荐使用MyBatisPlus来操作数据库的。它提供了插件的机制,我们可以通过拦截它提供的四大组件的某些对象,某些方法,来操作SQL,动态的为我们的SQL拼接上租户ID字段。

当然,MyBatis-Plus高版本提供了更加方便的拦截器,并且已经将多租户插件放入JAR包,我们只需稍加实现,并将该插件加入到MyBatis的拦截器链中,就可以不用再显式的拼接租户ID字段了,降低了出错的概率。

三、MyBatisPlus实现多租户功能

如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本。可以采用方案三,即共享数据库,共享数据架构,因为这种方案服务器成本最低,但是提高了开发成本。

所以MybatisPlus就提供了一种多租户的解决方案,实现方式是基于多租户插件TenantLineInnerInterceptor进行实现的。

在 MyBatis Plus 中,采用“共享数据库,共享数据架构”方式实现多租户。

MybatisPlus提供了租户处理器( TenantId 行级 ),租户之间共享数据库,共享数据架构,通过表字段(租户ID)进行数据逻辑隔离。

该种实现方式,需要我们在要实现多租户的表中添加 tenant_id(租户ID)字段,每次在对数据库操作时都需要在 where 后面添加租户判断条件“tenant_id=用户的租户ID”。

然而,使用了 MyBatis Plus 后,我们就不需要每次都手动在 wehre 后面添加 tenant_id 条件。

注意事项:

  • 多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!
  • 启用多租户后所有执行的method的sql都会进行处理.
  • 自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
<!-- Mybatis-Plus 增强CRUD -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

<!-- Mybatis-Plus 扩展插件 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
    <version>3.5.1</version>
</dependency>

TenantLineInnerInterceptor是MybatisPlus中提供的多租户插件,其使用方法大致分为下面4步:

3.1、表及实体类添加租户ID

应用添加维护一张tenant(租户表),记录租户的信息,每一个租户,有一个租户ID。

然后,在需要进行隔离的数据表上新增租户id,例如,现在有数据库表(user)如下:

租户ID一般用tenant_id

字段名字段类型描述
idLong主键
tenantIdLong租户编码
othervarchar(256)其他属性

将tenantId用来隔离租户与租户之间的数据,如果要查询当前服务商的用户,SQL大致如下:

SELECT * FROM table t WHERE  t.tenantId = 1;

3.2、application文件中添加多租户配置和新增配置属性类

(1)设置环境变量,配置拦截规则:

  • tenant.enable: 可以设置是否开启多租户,
  • tenant.ignoreTables:需要进行租户id过滤的表名集合。
  • tenant.filterTables:对多租户的表设置白名单忽略多租户拦截等。例如sys_user表结构中,没有tenant_id多租户字段,那么多租户拦截器不拦截该表。
#多租户配置
tenant:
  enable: true
  column: tenant_id
  filterTables:
  ignoreTables:
    - sys_app
    - sys_config
    - sys_dict_data
    - sys_dict_type
    - sys_logininfor
    - sys_menu
    - sys_notice
    - sys_oper_log
    - sys_role
    - sys_role_menu
    - sys_user
    - sys_user_role
  ignoreLoginNames:

(2)多租户配置属性类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
 
/**
 * 多租户配置属性类
 *
 * @author hege
 * @Date 2023-08-25
 *
 */
@Data
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
    /**
     * 是否开启多租户
     */
    private Boolean enable = true;
 
    /**
     * 租户id字段名
     */
    private String column = "tenant_id";
 
    /**
     * 需要进行租户id过滤的表名集合
     */
    private List<String> filterTables;
 
    /**
     * 需要忽略的多租户的表,此配置优先filterTables,若此配置为空,则启用filterTables
     */
    private List<String> ignoreTables;
 
    /**
     * 需要排除租户过滤的登录用户名
     */
    private List<String> ignoreLoginNames;
}

3.3、编写多租户处理器实现TenantLineHandler接口

在 MyBatis Plus 中,提供了 TenantLineInnerInterceptor 插件和 TenantLineHandler 接口。

其中:

  • TenantLineInnerInterceptor 插件用来自动向每个 SQL 的 where 后面添加判断条件“tenant_id=用户的租户ID”。
  • 而 TenantLineHandler 接口用来给 TenantLineInnerInterceptor 插件提供租户ID、租户字段名。

TenantLineHandler 接口定义如下:

public interface TenantHandler {
  /**
   * 获取租户 ID 值表达式,支持多个 ID 条件查询
   * 支持自定义表达式,比如:tenant_id in (1,2) @since 2019-8-2
   * @param where 参数 true 表示为 where 条件 false 表示为 insert 或者 select 条件
   * @return 租户 ID 值表达式
   */
  Expression getTenantId(boolean where);

  /**
   * 获取租户字段名
   * @return 租户字段名
   */
  String getTenantIdColumn();

  /**
   * 根据表名判断是否进行过滤
   * @param tableName 表名
   * @return 是否进行过滤, true:表示忽略,false:需要解析多租户字段
   */
  boolean doTableFilter(String tableName);
}

实现TenantHandler接口并实现它的方法,下面是一个例子:

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
 
/**
 * 多租户处理器实现TenantLineHandler接口
 *
 * @author hege
 * @Date 2023-08-25
 */
public class MultiTenantHandler implements TenantLineHandler {
 
    private final TenantProperties properties;
 
    public MultiTenantHandler(TenantProperties properties) {
        this.properties = properties;
    }
 
    /**
     * 获取租户ID值表达式,只支持单个ID值 (实际应该从用户信息中获取)
     *
     * @return 租户ID值表达式
     */
    @Override
    public Expression getTenantId() {
 
        //实际应该从用户信息中获取
        if(SecurityUtils.getTenantLoginUser()!=null)
        {
            //SecurityUtils 从ThreadLocal里面的安全上下文 中获取 用户所归属的单位id(租户id)
            Long tenantId = SecurityUtils.getLoginUser().getUser().getRootPartyId();
            if(tenantId!=null)
            {
                return new LongValue(tenantId);
            }
        }
 
        return new LongValue(0);
 
    }
 
    /**
     * 获取租户字段名,默认字段名叫: tenant_id
     *
     * @return 租户字段名
     */
    @Override
    public String getTenantIdColumn() {
        
        //通过配置获取
        return properties.getColumn();
    }
 
    /**
     * 根据表名判断是否忽略拼接多租户条件
     *
     * 默认都要进行解析并拼接多租户条件
     *
     * @param tableName 表名
     * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
     */
    @Override
    public boolean ignoreTable(String tableName) {
 
        //忽略指定用户对租户的数据过滤
        List<String> ignoreLoginNames=properties.getIgnoreLoginNames();
        
        //SecurityUtils 从ThreadLocal里面的安全上下文 中获取 用户名称
        String loginName=SecurityUtils.getTenantUsername();
        if(null!=ignoreLoginNames && ignoreLoginNames.contains(loginName)){
            return true;
        }
 
        //忽略指定表对租户数据的过滤
        List<String> ignoreTables = properties.getIgnoreTables();
        if (null != ignoreTables && ignoreTables.contains(tableName)) {
            return true;
        }
 
        return false;
    }
}

SecurityUtils 从ThreadLocal里面的安全上下文 中获取 用户名称, 用户所归属的单位id(租户id)

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.List;
import java.util.stream.Collectors;
 
/**
 * 安全服务工具类
 *
 * @author hege
 */
public class SecurityUtils {
 
   
    /**
     * 获取多租户用户
     **/
    public static LoginUser getTenantLoginUser() {
        try {
 
            LoginUser loginUser = null;
 
            // 获取安全上下文对象,就是那个保存在ThreadLocal里面的安全上下文对象,总是不为null(如果不存在,则创建一个authentication属性为null的empty安全上下文对象)
            SecurityContext securityContext = SecurityContextHolder.getContext();
            // 获取当前认证了的 principal(当事人) 或者 request token (令牌); 如果没有认证,会是 null,该例子是认证之后的情况
            Authentication authentication = securityContext.getAuthentication();
 
            if(authentication!=null)
            {
                if(authentication.getPrincipal()!=null)
                {
                    if (authentication.getPrincipal() instanceof LoginUser) {
                        loginUser = (LoginUser) authentication.getPrincipal();
                    }
 
                }
            }
            return loginUser;
 
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
        }
    }
 
}

3.4、MybatisPlus配置类启用多租户拦截插件

前面讲到,在 MyBatis Plus 中,提供了 TenantLineInnerInterceptor 插件和 TenantLineHandler 接口。

其中,TenantLineInnerInterceptor 插件用来自动向每个 SQL 的 where 后面添加判断条件“tenant_id=用户的租户ID”。

TenantLineInnerInterceptor 插件 调用 TenantLineHandler 接口用来给 提供租户ID、租户字段名。

使用 @Configuration 和 @Bean 注解配置 MyBatis Plus 的多租户插件,

iimport com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
/**
 * Mybatis Plus 配置
 *
 * @author hege
 */
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
@EnableConfigurationProperties(TenantProperties.class)
public class MybatisPlusConfig {
 
    /**
     * 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
     *
     * @param tenantProperties
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties) {
 
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        if (Boolean.TRUE.equals(tenantProperties.getEnable())) {
            // 启用多租户插件拦截
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MultiTenantHandler(tenantProperties)));
        }
 
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        // 阻断插件
        interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
 
        return interceptor;
    }
 
}

配置好之后,不管是查询、新增、修改删除方法,MP都会自动加上租户ID的标识,测试如下:

@Test
public void select(){
  List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().eq(User::getAge, 18));
  users.forEach(System.out::println);
}
运行sql实例:
DEBUG==> Preparing: SELECT id, login_name, name, password, 
      email, salt, sex, age, phone, user_type, status,
     organization_id, create_time, update_time, version,
     tenant_id FROM sys_user 
   WHERE sys_user.tenant_id = '001' AND is_delete = '0' AND age = ?

验证结果:

针对MybatisPlus提供的API、自定义Mapper中的statement均可正常拦截,会在SQL执行增删改查的时候自动加上tenant_id。

3.5、特定SQL语句忽略拦截

如果在程序中,有部分SQL不需要加上租户ID的表示,需要过滤特定的sql,或者对于一些超级管理员使用的接口,希望跨租户查询、免数据鉴权时,无需多租户拦截。

怎么办?

可以通过下面几种方式实现忽略拦截:

  • 方法1:使用MybatisPlus框架自带的@InterceptorIgnore注解,以用在Mapper类上,也可以用在方法上
  • 方法2:添加超级用户账号白名单,在自定义的Handler里进行逻辑判断,跳过拦截
  • 方法3:添加数据表白名单,在自定义的Handler里进行逻辑判断,跳过拦截

使用MybatisPlus框架自带的@InterceptorIgnore注解,以用在Mapper类上,也可以用在方法上, 下面是一个例子:

/**
 * 使用@InterceptorIgnore注解,忽略多租户拦截 <br/>
 * 注解@InterceptorIgnore可以用在Mapper类上,也可以用在方法上
 *
 * @param id
 * @return
 */
@InterceptorIgnore(tenantLine = "true")
UserOrgVO myFindByIdNoTenant(@Param(value = "id") Long id);

参考文献:

https://mp.weixin.qq.com/s/TR75wnxsXgFZ2ot1dOvX2w

https://mp.weixin.qq.com/s/CVTuEINWHCLue1oB7Yr3ng

https://mp.weixin.qq.com/s/Nl5Oll9GcF6JB8JvIb2YqA

https://zhuanlan.zhihu.com/p/420696556

https://blog.csdn.net/CSDN2497242041/article/details/132525117

说在最后:有问题可以找老架构取经

多租户相关的面试题,是非常常见的面试题。

以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。

最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来帮扶、领路。

尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+就业困难小伙伴,拿到了一个年薪100W的offer。

尼恩技术圣经系列PDF

  • 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
  • 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
  • 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
  • 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
  • 《大数据HBase学习圣经:一本书实现HBase学习自由》
  • 《大数据Flink学习圣经:一本书实现大数据Flink自由》
  • 《响应式圣经:10W字,实现Spring响应式编程自由》
  • 《Go学习圣经:Go语言实现高并发CRUD业务开发》

……完整版尼恩技术圣经PDF集群,请找尼恩领取

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/266978.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue+ElementUI+nodejs学生宿舍报修管理系统68ozj

本站是一个B/S模式系统&#xff0c;采用vue框架&#xff0c;MYSQL数据库设计开发&#xff0c;充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得学生宿舍信息管理系统管理工作系统化、规范化。本系统的使用使管理人员从繁重的工作中…

爬虫字典生成工具,CeWL使用教程

爬虫字典生成工具,CeWL使用教程 1.工具概述2.参数解析3.使用实例1.工具概述 CeWL 是一个 ruby 应用程序,它将给定的 URL 爬到指定的深度,可以选择跟随外部链接,并返回一个单词列表,然后可用于密码破解者 Cewl 是黑客武器库中的强大工具,因为它允许创建有针对性的单词列…

Python学习路线 - Python语言基础入门 - Python基础综合案例 - 数据可视化 - 地图可视化

Python学习路线 - Python语言基础入门 - Python基础综合案例 - 数据可视化 - 地图可视化 基础地图使用基础地图演示基础地图演示 - 视觉映射器 疫情地图-国内疫情地图案例效果数据整理 疫情地图-省级疫情地图省疫情地图 基础地图使用 基础地图演示 代码示例&#xff1a; &quo…

智能优化算法应用:基于蛇优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于蛇优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于蛇优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蛇优化算法4.实验参数设定5.算法结果6.参考文…

实习课知识整理4:点击某个商品如何跳转到并展示出商品详情页

项目情景&#xff1a;当我们点击某个商品时&#xff0c;我们需要查看商品的具体的信息并进行购买的操作 简单理解以下就是&#xff0c;当我们点击一个url链接时&#xff0c;该链接需要携带一个参数到后端&#xff0c;一般设为商品的Id&#xff0c;然后后端通过Id从数据库中查找…

idea structure视图介绍

作用 idea的Structure视图可以辅助查看代码结构 如何呼出Structure视图&#xff1f; Alt 7 Ctrl F12 侧边栏点Structure 我的常用配置 1、选Show Toolbar&#xff0c;便于使用功能按钮 2、使用Float视图&#xff0c;悬浮于窗口表面&#xff0c;可以使用 ShiftEsc来退出…

工业互联网:数字化制造的未来

引言 在当今的数字化时代&#xff0c;制造业正经历着革命性的变革。工业互联网&#xff08;Industrial Internet of Things"&#xff0c;简称 IIoT&#xff09;作为这一变革的核心引擎&#xff0c;正在重新定义现代工业和制造。本文将探讨工业互联网的基础、关键技术、应…

【SpringCloud笔记】(8)服务网关之GateWay

GateWay 概述简介 官网地址&#xff1a; 上一代网关Zuul 1.x&#xff1a;https://github.com/Netflix/zuul/wiki&#xff08;有兴趣可以了解一下&#xff09; gateway&#xff1a;https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/…

【开放集检测】OpenGAN: Open-Set Recognition via Open Data Generation 论文阅读

文章目录 英语积累为什么使用GAN系列网络进行开放集检测摘要1. 前言2. 相关工作开集检测基于GAN网络的开集检测基于暴露异常数据的开集检测 3. OpenGAN3.1 公式建模3.1.1 二分类方法存在问题如何解决 3.1.2 使用合成数据存在问题如何解决 3.1.3 OpenGAN3.1.4 模型验证 3.2 先前…

机器视觉工程师,面对难以实现的需求时,应该如何应对?

作为一名机器视觉工程师&#xff0c;在工作中难免会遇到一些难以实现&#xff0c;奇形怪状的需求&#xff0c;各种五花八门&#xff0c;奇葩需求&#xff0c;顿时头疼不已。同时销售要接订单&#xff0c;机器视觉工程师也要做项目提升自我&#xff0c;销售与技术矛盾本身是存在…

vivado 输出延迟

输出延迟 set_output_delay命令指定输出端口相对于设计接口处的时钟边缘。 当考虑应用板时&#xff0c;此延迟表示以下各项之间的相位差&#xff1a; 1.数据从FPGA的输出封装引脚通过板传播到另一个设备&#xff0c;以及 2.相对基准板时钟。 输出延迟值可以是正的&#xf…

基于javaSpringbootmysql的小型超市商品展销系统01635-计算机毕业设计项目选题推荐(免费领源码)

摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运用中&#xff0c;应用软件的工作…

AndroidStudio无法新建aidl文件解决办法

我用的 AS 版本是 Android Studio Giraffe | 2022.3.1 Build #AI-223.8836.35.2231.10406996, built on June 29, 2023 右键新建 aidl 文件&#xff0c; 提示 (AIDL File)Requires setting the buildFeatures.aidl to true in the build file 解决办法 修改 app 的 build.…

第26关 K8s日志收集揭秘:利用Log-pilot收集POD内业务日志文件

------> 课程视频同步分享在今日头条和B站 大家好&#xff0c;我是博哥爱运维。 OK&#xff0c;到目前为止&#xff0c;我们的服务顺利容器化并上了K8s&#xff0c;同时也能通过外部网络进行请求访问&#xff0c;相关的服务数据也能进行持久化存储了&#xff0c;那么接下来…

C语言进阶---------作业复习

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

MySQL升级版本(Linux环境)

摘要 由于我们在做部署的时候会部署MySQL&#xff0c;但是版本可能各种各样&#xff0c;而且我们服务器会定期的进行漏洞扫描&#xff0c;因此我们在遇到MySQL的相关漏洞时&#xff0c;一般漏洞报告中会提示出解决方案&#xff0c;一般来时就是升级软件的版本&#xff0c;因此…

虚拟机安装

带你解密Linux的【Vm】-CSDN博客https://blog.csdn.net/lz17267861157/article/details/134031133

浏览器渲染原理

上篇&#xff1a;事件循环机制&#xff0c;阅读理解上篇之后本文会更容易理解些。 浏览器是如何渲染页面的&#xff1f; 当浏览器的网络线程收到HTML文档后&#xff0c;会产生一个渲染任务&#xff0c;并将其传递给渲染主线程的消息队列。 整个渲染流程分为多个阶段&#xff…

安装Kubernetes1.23、kubesphere3.4、若依项目自动打包部署到K8S记录

1.安装kubernetes1.23详细教程 kubernetes(k8s)集群超级详细超全安装部署手册 - 知乎 2.安装rancher动态存储 kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml3.安装kubesphere3.4 准备工作 您…

JavaScript状态模式

JavaScript状态模式 1 什么是状态模式2 使用状态模式改造电灯程序3 缺少抽象类的变通方式4 示例&#xff1a;文件上传4.1 场景描述4.2 代码过程 1 什么是状态模式 允许一个对象在其内部状态改变时改变它的行为&#xff0c;对象看起来似乎修改了它的类。 比如说这样一个场景&a…