必修-场景题

1. 树遍历

遍历树是数据结构中的一个重要部分
二叉树(每个节点最多有两个子节点)
三叉树(每个节点最多有三个子节点)

通常,我们会有以下几种遍历方式:

前序遍历:访问顺序是根节点 -> 左子树 -> 右子树。
中序遍历 :访问顺序是左子树 -> 根节点 -> 右子树。
后序遍历:访问顺序是左子树 -> 右子树 -> 根节点。
层序遍历:按层次依次访问节点。

二叉树

首先,我们定义一个二叉树节点类:

class BinaryTreeNode {
    int value;
    BinaryTreeNode left;
    BinaryTreeNode right;

    BinaryTreeNode(int value) {
        this.value = value;
        left = null;
        right = null;
    }
}

接下来,我们实现各种遍历方法:

import java.util.LinkedList;
import java.util.Queue;

public class BinaryTreeTraversal {

    // 前序遍历
    public void preOrder(BinaryTreeNode node) {
        if (node != null) {
            System.out.print(node.value + " ");
            preOrder(node.left);
            preOrder(node.right);
        }
    }

    // 中序遍历
    public void inOrder(BinaryTreeNode node) {
        if (node != null) {
            inOrder(node.left);
            System.out.print(node.value + " ");
            inOrder(node.right);
        }
    }

    // 后序遍历
    public void postOrder(BinaryTreeNode node) {
        if (node != null) {
            postOrder(node.left);
            postOrder(node.right);
            System.out.print(node.value + " ");
        }
    }

    // 层序遍历
    public void levelOrder(BinaryTreeNode root) {
        if (root == null) return;
        Queue<BinaryTreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            BinaryTreeNode node = queue.poll();
            System.out.print(node.value + " ");
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }

    public static void main(String[] args) {
    	
        BinaryTreeNode root = new BinaryTreeNode(1);
        root.left = new BinaryTreeNode(2);
        root.right = new BinaryTreeNode(3);
        root.left.left = new BinaryTreeNode(4);
        root.left.right = new BinaryTreeNode(5);

        BinaryTreeTraversal traversal = new BinaryTreeTraversal();
        System.out.println("Pre-order:");
        traversal.preOrder(root);
        System.out.println("\nIn-order:");
        traversal.inOrder(root);
        System.out.println("\nPost-order:");
        traversal.postOrder(root);
        System.out.println("\nLevel-order:");
        traversal.levelOrder(root);
    }
}

三叉树

首先,我们定义一个三叉树节点类:

class TernaryTreeNode {
    int value;
    TernaryTreeNode left;
    TernaryTreeNode middle;
    TernaryTreeNode right;

    TernaryTreeNode(int value) {
        this.value = value;
        left = null;
        middle = null;
        right = null;
    }
}

接下来,我们实现各种遍历方法:

import java.util.LinkedList;
import java.util.Queue;

public class TernaryTreeTraversal {

    // 前序遍历
    public void preOrder(TernaryTreeNode node) {
        if (node != null) {
            System.out.print(node.value + " ");
            preOrder(node.left);
            preOrder(node.middle);
            preOrder(node.right);
        }
    }

    // 后序遍历
    public void postOrder(TernaryTreeNode node) {
        if (node != null) {
            postOrder(node.left);
            postOrder(node.middle);
            postOrder(node.right);
            System.out.print(node.value + " ");
        }
    }

    // 层序遍历
    public void levelOrder(TernaryTreeNode root) {
        if (root == null) return;
        Queue<TernaryTreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            TernaryTreeNode node = queue.poll();
            System.out.print(node.value + " ");
            if (node.left != null) queue.offer(node.left);
            if (node.middle != null) queue.offer(node.middle);
            if (node.right != null) queue.offer(node.right);
        }
    }

    public static void main(String[] args) {
        TernaryTreeNode root = new TernaryTreeNode(1);
        root.left = new TernaryTreeNode(2);
        root.middle = new TernaryTreeNode(3);
        root.right = new TernaryTreeNode(4);
        root.left.left = new TernaryTreeNode(5);
        root.left.middle = new TernaryTreeNode(6);
        root.left.right = new TernaryTreeNode(7);

        TernaryTreeTraversal traversal = new TernaryTreeTraversal();
        System.out.println("Pre-order:");
        traversal.preOrder(root);
        System.out.println("\nPost-order:");
        traversal.postOrder(root);
        System.out.println("\nLevel-order:");
        traversal.levelOrder(root);
    }
}

2. 并发问题

我以设计一个抢优惠券并发场景的解决方案,来举例,那么需要确保系统的高并发处理能力、安全性和可靠性。下面是我想到的解决方案:

营销模块优惠卷架构设计

前端

用户请求:使用Vue.js、React等前端框架进行用户交互设计,确保用户体验。
请求限流:前端可以进行简单的限流,比如每个用户每秒钟只能发起一个请求,用限流器进行限流防抖操作

function throttle(func, wait) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= wait) {
      lastTime = now;
      func.apply(this, args);
    }
  };
}
后端
  1. 进网管网关层
    使用Nginx、Spring Cloud Gateway等,进行负载均衡、限流和日志记录
Nginx
确保Nginx编译时带有 ngx_http_limit_req_module 模块,这是Nginx默认包含的模块。
配置限流:

 http {
 limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
 server {
            listen 80;
            server_name example.com;

            location /some/api/method {
                limit_req zone=one burst=5 nodelay;
                proxy_pass http://backend;
            }
        }
}

在这个配置中,limit_req_zone 定义了一个共享内存区域 one,用于保存限流信息。rate=1r/s 表示允许每秒一次请求。burst=5 允许突发流量可以达到5个请求。nodelay 禁用延迟队列。

Spring Cloud Gateway和限流的依赖:
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>


配置路由并启用限流:

在 application.yml 中配置限流。

    spring:
      cloud:
        gateway:
          routes:
          - id: some_api
            uri: http://backend-service
            predicates:
            - Path=/some/api/method
            filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 5

确保你有 Redis 依赖和配置: Spring Cloud Gateway 使用 Redis 进行限流,这意味着你需要配置 Redis 相关的依赖和配置

  1. 服务层
    应用服务:采用微服务架构,将抢优惠券逻辑放在专门的Coupon Service中。
    使用 Redis/Memcached:用于缓存优惠券信息,以及用户抢券请求,防止数据库压力过大
处理优惠券的缓存逻辑:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
**
 * Author: 徐寿春
 * Date:    2024/7/25 17:40
 * <p>
 * 名称  处理优惠券的缓存逻辑
 */
@Service
public class CouponService {

    private static final String COUPON_KEY_PREFIX = "coupon:";
    private static final String USER_REQUEST_KEY_PREFIX = "user_request:";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 缓存优惠券信息
    public void cacheCouponInfo(String couponId, String couponInfo, long timeout, TimeUnit unit) {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        ops.set(COUPON_KEY_PREFIX + couponId, couponInfo, timeout, unit);
    }

    // 获取缓存的优惠券信息
    public String getCouponInfo(String couponId) {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        return (String) ops.get(COUPON_KEY_PREFIX + couponId);
    }

    // 记录用户抢券请求
    public boolean recordUserRequest(String userId, String couponId, long timeout, TimeUnit unit) {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        String key = USER_REQUEST_KEY_PREFIX + userId + ":" + couponId;
        
        if (redisTemplate.hasKey(key)) {
            return false; // 用户已经抢过该券
        } else {
            ops.set(key, "requested", timeout, unit);
            return true;
        }
    }
}
处理优惠卷HTTP请求:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
**
 * Author: 徐寿春
 * Date:    2024/7/25 17:40
 * <p>
 * 名称  处理优惠券逻辑
 */
@RestController
@RequestMapping("/coupons")
public class CouponController {

    @Autowired
    private CouponService couponService;

    @GetMapping("/{couponId}")
    public String getCouponInfo(@PathVariable String couponId) {
        String couponInfo = couponService.getCouponInfo(couponId);
        if (couponInfo == null) {
            // 从数据库中获取优惠券信息,并缓存起来
            couponInfo = "Coupon info from DB"; // 这里应该从数据库获取
            couponService.cacheCouponInfo(couponId, couponInfo, 1, TimeUnit.HOURS);
        }
        return couponInfo;
    }

    @PostMapping("/{couponId}/request")
    public String requestCoupon(@PathVariable String couponId, @RequestParam String userId) {
        boolean success = couponService.recordUserRequest(userId, couponId, 1, TimeUnit.DAYS);
        if (success) {
            return "Coupon requested successfully";
        } else {
            return "Coupon already requested";
        }
    }
}

二, Token Bucket:采用令牌桶算法进行流量控制,限制每秒钟的请求数。

实现令牌桶算法
import org.springframework.stereotype.Component;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
**
 * Author: 徐寿春
 * Date:    2024/7/25 17:40
 * <p>
 * 名称  延时线程池生成token
 */
@Component
public class TokenBucket {

    private final int MAX_TOKENS = 10; // 最大令牌数
    private int tokens;
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    public TokenBucket() {
        this.tokens = MAX_TOKENS;
        startTokenGenerator();
    }

    // 定期生成令牌
    private void startTokenGenerator() {
        scheduler.scheduleAtFixedRate(() -> {
            synchronized (this) {
                if (tokens < MAX_TOKENS) {
                    tokens++;
                }
                System.out.println("Tokens available: " + tokens);
            }
        }, 0, 1, TimeUnit.SECONDS); // 每秒钟生成一个令牌
    }

    // 请求令牌
    public synchronized boolean requestToken() {
        if (tokens > 0) {
            tokens--;
            return true;
        }
        return false;
    }
}

请求限流一秒
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

**
 * Author: 徐寿春
 * Date:    2024/7/25 17:40
 * <p>
 * 名称  限流请求token算法令牌接口
 */
@RestController
public class RateLimitController {

    @Autowired
    private TokenBucket tokenBucket;

    @GetMapping("/limited-endpoint")
    public String limitedEndpoint() {
        if (tokenBucket.requestToken()) {
            return "Request processed successfully.";
        } else {
            return "Too many requests. Please try again later.";
        }
    }
}
  1. 数据库层
    关系型数据库:MySQL,用于存储持久化的优惠券数据。
    NoSQL数据库:使用Redis的持久化功能或者MongoDB用于用户临时抢券信息存储。
    MySQL:用于存储需要持久化的、结构化的优惠券数据(长期保存)。
    Redis:用于存储高并发、需要快速读写的用户临时抢券信息(短期保存,高性能)。

  2. 并发控制策略
    a. 乐观锁
    在数据库层面使用乐观锁机制避免超发优惠券。例如,在优惠券数量减少的过程中,进行版本号比较,确保操作的原子性,前提是一个库一张表
    b. 分布式锁
    使用Redis分布式锁(或者ZooKeeper等)确保优惠券扣减的原子性,可避免并发超发,但要考虑延时问题
    c. 请求去重
    使用独立的请求ID对每个用户的请求进行去重,防止重复请求,常用的就是id加上ip加上机器放bitmap
    d. 延迟队列
    对于高并发的场景,可以采用Kafka/RabbitMQ等消息队列,将请求进行排队处理,避免瞬时高并发冲击数据库,关于如何利用消息队列延时队列处理有对应的文章我集成框架 - RabbitMQ

  3. 流程设计
    用户请求:用户发送抢优惠券请求。
    网关层限流:网关层进行初步限流和鉴权。
    缓存层验证:查询Redis缓存中的优惠券是否仍有剩余。
    如果有,进入下一步。
    如果没有,直接返回抢光提示。
    分布式锁:在Redis中获取分布式锁,确保同一时间只有一个请求进行优惠券扣减操作。
    数据库操作:
    开启事务。
    查询当前优惠券库存。
    扣减库存,更新数据。
    提交事务。
    释放锁:释放Redis分布式锁。
    更新缓存:同步更新Redis中的优惠券库存信息。
    响应用户:返回成功领取的响应。

我能想到就这么多,剩下的自己补充

  1. 错误处理与降级策略
    超时处理:设置合理的请求超时时间,超时后提示用户重试,关于系统请求重试机制我写一下吧,以http为例
**
 * Author: 徐寿春
 * Date:    2024/7/25 17:58
 * <p>
 * 名称  重试demo
 */
public class RetryHttpRequest {

    // 定义重试次数和间隔时间
    private static final int MAX_RETRIES = 3;
    private static final int RETRY_INTERVAL_MS = 1000; // 1秒钟

    public static void main(String[] args) {
        String urlString = "http://example.com";
        for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
            try {
                String response = sendHttpRequest(urlString);
                System.out.println("请求成功: " + response);
                // 成功时跳出循环
                break;
            } catch (SocketTimeoutException e) {
                System.out.println("请求超时,尝试重试 " + attempt);
                if (attempt == MAX_RETRIES) {
                    System.out.println("达到最大重试次数,停止重试");
                } else {
                    try {
                        Thread.sleep(RETRY_INTERVAL_MS);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        System.out.println("线程被中断");
                    }
                }
            } catch (IOException e) {
                System.out.println("请求失败: " + e.getMessage());
                // 其他IOException错误直接停止重试
                break;
            }
        }
    }

    public static String sendHttpRequest(String urlString) throws IOException {
        URL url = new URL(urlString);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setConnectTimeout(5000); // 设置连接超时时间
        connection.setReadTimeout(5000); // 设置读取超时时间

        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) { // 200表示成功
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder response = new StringBuilder();

            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
            in.close();
            return response.toString();
        } else {
            throw new IOException("HTTP 请求失败,响应码: " + responseCode);
        }
    }
}

降级策略:当系统压力大或出现问题时,可降级处理,例如直接返回优惠券已抢光,或者进入排队模式,Hystrix太重了我平时用Resilience4j

用Resilience4j实现降级策略
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>1.7.1</version>
</dependency>
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;

import java.time.Duration;
import java.util.function.Supplier;

**
 * Author: 徐寿春
 * Date:    2024/7/25 18:28
 * <p>
 * 名称  Resilience4j demo 降级
 */
public class Resilience4jExample {

    public static void main(String[] args) {
        // 创建配置
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .slidingWindowSize(2)
                .build();

        // 创建CircuitBreaker
        CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(circuitBreakerConfig);
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("couponService");

        // 定义业务逻辑
        Supplier<String> getCouponSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
            if (System.currentTimeMillis() % 2 == 0) {
                throw new RuntimeException("System is busy");
            }
            return "Coupon acquired!";
        });

        // 定义降级逻辑
        Supplier<String> fallbackSupplier = () -> "Coupons are sold out, please try again later.";

        // 执行并处理降级
        String result = CircuitBreaker.decorateSupplier(circuitBreaker, getCouponSupplier)
                                       .recover(fallbackSupplier)
                                       .get();
        
        System.out.println(result);
    }
}

Resilience4j 这块我写一下吧,回头做个和Hystrix的比较

   <dependency>
       <groupId>io.github.resilience4j</groupId>
       <artifactId>resilience4j-spring-boot2</artifactId>
       <version>1.7.0</version>
   </dependency>
在 application.yml 或 application.properties 中配置 Resilience4j:
   resilience4j:
     circuitbreaker:
       configs:
         default:
           registerHealthIndicator: true
           slidingWindowSize: 100
           permittedNumberOfCallsInHalfOpenState: 10
           minimumNumberOfCalls: 10
           waitDurationInOpenState: 10s
           failureRateThreshold: 50
           eventConsumerBufferSize: 10
       instances:
         backendA:
           baseConfig: default
           ----
registerHealthIndicator:
解释: 当设置为 true 时,Resilience4j 会注册一个健康指示器(Health Indicator),使其可用于监控框架,例如 Spring Boot Actuator。

slidingWindowSize:
解释: 滑动窗口的大小。表示断路器的滑动窗口会包含多少次调用。100表明滑动窗口包含100次调用。

permittedNumberOfCallsInHalfOpenState:
解释: 半开启状态时允许的最大调用次数。当断路器从开启状态转换为半开启状态时,允许通过的最大调用次数。10表示允许10次调用。

minimumNumberOfCalls:
解释: 在评价断路器状态之前,必须记录的最少调用次数。如果设为 10,那么在至少有10次调用后,才会根据配置的其他条件再来决定断路器的状态。

waitDurationInOpenState:
解释: 断路器在开启状态保持的时间长度。10s 表示在断路器转换为开放状态后,10秒钟后将进入半开启状态。

failureRateThreshold:
解释: 失败率的阈值。定义了错误调用率的最大百分比。如果设为 50,那么只有当失败调用比例超过50%时,断路器才会打开。

eventConsumerBufferSize:
解释: 事件消费者的缓冲区大小,表示事件处理队列的最大容量。设为 10的话,表示最多可以处理10个事件。如果满了,新事件将覆盖最老的事件。

控制层

   @RestController
   @RequestMapping("/api")
   public class BackendController {
   
       private final BackendService backendService;
   
       @Autowired
       public BackendController(BackendService backendService) {
           this.backendService = backendService;
       }

       @GetMapping("/doSomething")
       public ResponseEntity<String> doSomething() {
           String result = backendService.doSomething();
           return new ResponseEntity<>(result, HttpStatus.OK);
       }
   }

服务实现

   @Service
   public class BackendServiceImpl implements BackendService {
   
       @Override
       @CircuitBreaker(name = "backendA", fallbackMethod = "fallback")
       public String doSomething() {
           // 这里模拟一个可能会失败的调用
           if (new Random().nextBoolean()) {
               throw new RuntimeException("Service failed");
           }
           return "Service is successful";
       }
   
       // 断路器打开时的回退方法
       public String fallback(Throwable t) {
           return "Service is down, please try again later";
       }
   }

 resilience4j:
     retry:
       instances:
         backendA:
           maxAttempts: 3
           waitDuration: 500ms
           retryExceptions:
             - java.io.IOException
             - java.util.concurrent.TimeoutException


 
retry: 这部分配置是关于重试机制的。

backendA: 这是特定于某个服务或组件(在这里是 backendA)的配置。这里列出的所有配置都只适用于 backendA。

maxAttempts: 3: 这是最大重试次数。当对 backendA 进行操作失败时,最多会重试两次(加上第一次尝试,总共三次)。也就是说,最多会允许三次失败尝试。

waitDuration: 500ms: 重试之间的等待时间是 500 毫秒。如果一次尝试 backendA 失败,那么在最小 500 毫秒后,库会再次尝试。

retryExceptions: 这是一个需要触发重试的异常列表。如果遇到列出的异常,重试机制就会生效。

- java.io.IOException: 遇到 java.io.IOException 异常时,会触发重试。

- java.util.concurrent.TimeoutException: 遇到 java.util.concurrent.TimeoutException 异常时,也会触发重试。

修改服务实现:

   @Service
   public class BackendServiceImpl implements BackendService {
   
       @Override
       @Retry(name = "backendA", fallbackMethod = "fallback")
       public String doSomething() {
           // 这里模拟一个可能会失败的调用
           if (new Random().nextBoolean()) {
               throw new RuntimeException("Service failed");
           }
           return "Service is successful";
       }
   
       public String fallback(Throwable t) {
           return "Service is down, please try again later";
       }
   }
  1. 监控与报警:使用Prometheus、Grafana等进行系统监控,设置报警阈值,及时发现并处理问题,这部分回头补吧

3.匹配算法逻辑题

有一个字符串构成结构为 北京 杭州 杭州 北京 要求写一个匹配逻辑

举例: pattern = "abba" str = "北京 杭州 杭州 北京 " 返回 true
举例: pattern = "aabb" str = "北京 杭州 杭州 北京 " 返回 false
举例: pattern = "baab" str = "北京 杭州 杭州 北京 " 返回 false

逻辑如下

    public static boolean matchPattern(String str, String pattern) {
        String[] words = pattern.split(" ");
        if (str.length() != words.length) {
            return false; // 如果字符串长度和单词数组长度不一致
        }

        Map<Object, Integer> mapping = new HashMap<>();

        for (int i = 0; i < str.length(); i++) {
            //得到 a
            char c = str.charAt(i);
            //得到杭州
            String word = words[i];

            // 同时检查字符和单词,确保他们的索引一致
            if (!mapping.put(c, i).equals(mapping.put(word, i))) {
                return false; // 如果索引不一致,返回 false
            }
        }

        return true;  // 如果循环中没有冲突,返回 true
    }
}

4. jvm方面

基础概念

Java 垃圾回收器(Garbage 垃圾桶 Collector 控制,简称 GC)以下是我整理的常见的垃圾回收器及其在不同 JDK 版本中的支持情况:

  1. Serial GC 串行垃圾回收器
    常用于小型应用或单线程环境中 JDK 1.2 及以上

  2. Parallel GC 并行垃圾回收器
    专注于高吞吐量和短暂停顿时间,是大多数服务器应用程序的默认选择,JDK 1.4 及以上

  3. CMS 侧重于减少垃圾回收的停顿时间,并发执行垃圾回收操作。适用于低延迟应用。
    虽然在 JDK 9 中被标记为过时(Deprecated),并在 JDK 14 中被移除。JDK 1.4 - JDK 13

  4. G1 设计用于大内存、多处理器的环境,能够更好地控制停顿时间。分区垃圾回收,优先回收最耗时的区域。
    设计用于取代 CMS GC,并在 JDK 9 中成为默认垃圾回收器。JDK 7 及以上

  5. ZGC 具有非常低的停顿时间,并且可以处理非常大的堆内存(多达数 TB 的内存)。
    专注于极低的延迟,适用于需要大量内存的应用程序,JDK 11 及以上

  6. Shenandoah GC 是 Red Hat 开发的一种低停顿垃圾回收器,像 ZGC 一样,目标是实现几乎恒定的停顿时间。
    另一种低延迟垃圾回收器,与 ZGC 类似,JDK 12 及以上

  7. Epsilon GC
    一个实验性的 “无操作” 垃圾回收器,不进行任何垃圾回收,适合做性能测试和调试用途,JDK 11 及以上

默认 GC 改变

JDK 8:Parallel GC 为默认垃圾回收器。
JDK 9:G1 GC 成为默认垃圾回收器。

垃圾回收算法

  • 标记-清除算法

Description: 分两个阶段,首先是标记出所有存活的对象,然后清除未被标记的对象。标记阶段可能会导致应用停顿。

  • 标记-压缩算法

Description: 与标记-清除类似,但在清除阶段会将存活的对象压缩到堆的一端,防止产生碎片。

  • 复制算法

Description: 将活动对象复制到新区域(通常是“新生代”内存区),原区域被清空。适用于新生代的垃圾回收,减少碎片。

  • 分代收集算法

Description: 将堆分为新生代和老年代,不同代使用不同的回收算法。新生代使用复制算法,老年代使用标记-清除或标记-压缩。

  • 卡表算法

Description: 通过记录对象的引用更新来优化并发垃圾回收器的性能。根部引用发

  • 并发标记-扫描算法

Description: 标记阶段和清除阶段都是并发进行的,以减少垃圾回收对应用的影响。

常用调优命令会问到的整理

  1. 内存配置
    -Xms:设置 JVM 初始堆内存大小。
    -Xmx:设置 JVM 最大堆内存大小。
    -Xmn:设置 JVM 年轻代(young generation)内存大小。
    -XX:PermSize=:设置永久代(permanent generation)初始大小(Java 8 以前)。
    -XX:MaxPermSize=:设置永久代最大大小(Java 8 以前)。
    -XX:MetaspaceSize=:设置元空间初始大小(Java 8 及以后)。
    -XX:MaxMetaspaceSize=:设置元空间最大大小(Java 8 及以后)

  2. 垃圾回收器(GC)选项
    -XX:+UseSerialGC:使用串行垃圾回收器。
    -XX:+UseParallelGC:使用并行垃圾回收器(适用于多核 CPU)。
    -XX:+UseConcMarkSweepGC:使用并发标记清除(CMS)垃圾回收器。
    -XX:+UseG1GC:使用 G1 垃圾回收器(Java 7 更新 4及以后推荐)。

  3. GC 调优参数
    -XX:NewRatio=:设置年轻代和老年代的比值。
    -XX:SurvivorRatio=:设置 Eden 区和 Survivor 区的比值。
    -XX:MaxTenuringThreshold=:设置对象晋升到老年代的年龄阈值。
    -XX:+UseGCLogFileRotation:启用 GC 日志文件轮转。
    -XX:NumberOfGCLogFiles=:设置 GC 日志文件轮转的数量。
    -XX:GCLogFileSize=:设置每个 GC 日志文件的大小。

  4. GC 日志
    -XX:+PrintGC:简单的 GC 统计信息。
    -XX:+PrintGCDetails:详细的 GC 统计信息。
    -XX:+PrintGCTimeStamps:在 GC 日志中打印时间戳。
    -XX:+PrintGCDateStamps:在 GC 日志中打印日期。
    -Xloggc::将 GC 日志输出到文件。

  5. 性能调优参数
    -XX:+AggressiveOpts:启用一些性能优化选项。
    -XX:+UseStringDeduplication:运行字符串去重(在使用 G1 GC 时)。

  6. 其他有用的选项
    -Djava.awt.headless=true:在没有显示器的环境中运行 Java 应用(例如在服务器上)。
    -XX:+HeapDumpOnOutOfMemoryError:在发生内存溢出时生成堆转储文件。
    -XX:HeapDumpPath=:指定堆转储文件的路径。
    -XX:+ExitOnOutOfMemoryError:在内存溢出时结束 JVM。

基本示例
java -Xms512m -Xmx2048m -XX:+UseG1GC -XX:+PrintGCDetails -Xloggc:/path/to/gc.log -jar myapp.jar

以上命令设置了初始堆内存为 512MB,最大堆内存为 2048MB,使用 G1 GC,并输出详细的 GC 日志到 /path/to/gc.log。

5. 网络方面

使用Spring Boot实现WebSocket通信

  1. 添加依赖
    首先,你需要在pom.xml文件中添加Spring WebSocket的依赖。
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
</dependencies>
  1. WebSocket配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
 * Author: 徐寿春
 * Date:    2024/7/30 11:48
 * <p>
 * 名称
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 使用简单的WebSocket处理器
        registry.addHandler(new MyWebSocketHandler(), "/ws").setAllowedOrigins("*");
    }
}
  1. WebSocket处理器
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class MyWebSocketHandler extends TextWebSocketHandler {

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("Received: " + payload);
        
        // 处理消息并进行响应
        session.sendMessage(new TextMessage("Hello, " + payload));
    }
}
  1. 前端JavaScript连接WebSocket 手写一个测试html
<!DOCTYPE html>
<html>
<head>
    <title>xscWebSocket Test</title>
</head>
<body>
    <script type="text/javascript">
        const socket = new WebSocket('ws://localhost:8080/ws');

        socket.onopen = function(event) {
            console.log('WebSocket is open now.');
            socket.send('Hello Server');
        };

        socket.onmessage = function(event) {
            console.log('Received Message: ' + event.data);
        };

        socket.onclose = function(event) {
            console.log('WebSocket is closed now.');
        };

        socket.onerror = function(error) {
            console.log('WebSocket Error: ' + error);
        };
    </script>
</body>
</html>

启动Spring Boot应用。WebSocket端点将会在/ws路径上提供服务,WebSocketConfig类用于配置WebSocket端点和转发器,MyWebSocketHandler类用于处理WebSocket消息,前端的JavaScript用于连接和与WebSocket服务器通信,可以在MyWebSocketHandler中添加更多处理逻辑,例如处理二进制消息、实现心跳检测、处理不同类型的消息等。此外,你也可以结合Spring Security来保护你的WebSocket连接。

使用Netty作为通信

在pom.xml文件中添加Netty的依赖:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.65.Final</version>
</dependency>

编写Netty服务器

package com.example.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
 * Author: 徐寿春
 * Date:    2024/7/30 11:48
 * <p>
 * 名称
 */
@Component
public class NettyServer {

    private final int port = 8080; // 可以选择任何开放端口

    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    @PostConstruct
    public void start() throws Exception {
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                           .channel(NioServerSocketChannel.class)
                           .childHandler(new ChannelInitializer<SocketChannel>() {
                               @Override
                               protected void initChannel(SocketChannel ch) throws Exception {
                                   ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new SimpleChannelHandler());
                               }
                           })
                           .option(ChannelOption.SO_BACKLOG, 128)
                           .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            System.out.println("Netty server started on port: " + port);

            channelFuture.channel().closeFuture().sync();
        } finally {
            shutdown();
        }
    }

    @PreDestroy
    public void shutdown() throws InterruptedException {
        if (bossGroup != null) {
            bossGroup.shutdownGracefully().sync();
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully().sync();
        }
    }
}

SimpleChannelHandler.java

package com.example.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
 * Author: 徐寿春
 * Date:    2024/7/30 11:48
 * <p>
 * 名称
 */
public class SimpleChannelHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println("Received message: " + msg);
        ctx.writeAndFlush("Message received: " + msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

NettyApplication.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * Author: 徐寿春
 * Date:    2024/7/30 11:48
 * <p>
 * 名称
 */
@SpringBootApplication
public class NettyApplication {
    public static void main(String[] args) {
        SpringApplication.run(NettyApplication.class, args);
    }
}

运行NettyApplication主类,启动Spring Boot应用,设置端口80,Netty服务器会在8080绑定。
使用任何TCP客户端工具连接到localhost的8080端口,发送消息 可以额看到服务器端的响应

1. 通过 bind 方法绑定不同的端口 8080
2. closeFuture 用于监听每个绑定端口的关闭事件
3. EventLoopGroup 用于处理 Netty 的事件循环。
4. 接受数据打印: System.out.println("Received message: " + msg);'

Http 和 Https

基本概念

HTTP(HyperText Transfer Protocol, 超文本传输协议)
HTTPS(HTTP Secure)是在HTTP之上添加了SSL/TLS加密层,以保证数据传输的安全性


HTTP请求过程

HTTP是一个无状态协议,即每个请求都是独立的,无需保留之前请求的状态。典型的HTTP请求过程如下:

  1. 客户端与服务器建立TCP连接
  2. 客户端发送HTTP请求到服务器。
  3. 服务器处理请求并返回HTTP响应。
  4. 连接关闭(HTTP/1.0)或保持(HTTP/1.1)

HTTP/1.0 vs HTTP/1.1 vs HTTP/2 vs HTTP/3
  1. HTTP/1.0: 每个请求/响应都需要一个新的TCP连接。
  2. HTTP/1.1: 支持持久连接和分块传输编码,允许复用TCP连接。
  3. HTTP/2: 引入了二进制分帧、多路复用、头部压缩和服务器推送等特性,大幅提高了传输性能。
  4. HTTP/3: 基于QUIC传输协议,进一步改善性能和安全性。
HTTP网络模型
  1. OSI七层模型(物理层、表示层和应用层,网络层、传输层、会话层 、数据链路层)

  2. HTTP网络模型实际上基于更底层的TCP/IP五层模型从上到下分别是:(应用层,传输层,网络层,数据链路层,物理层)

  • 应用层(HTTP协议所在层)处理与应用相关的逻辑操作,例如网页的请求和响应,HTTP协议用于指定要传输的请求和响应的格式。
  • 传输层(TCP协议所在层)负责数据的可靠传输(通过TCP协议),建立连接(通过TCP的三次握手)并保证数据传输的完整性。
  • 网络层(IP协议所在层) 负责数据包的路由选择和传输,IP协议定义每个设备的地址并管理数据包从源到目的地的传输。
“粘包”和“拆包”的问题

粘包是说在TCP传输中,接收方在一次接收操作中读取到多个独立的HTTP请求或响应。白话就是说,多个消息在传输时被粘在了一起了,导致接收方一次读取时得到的数据包含了多个完整的或不完整的消息,大致长这样

HTTP/1.1: 支持持久连接和分块传输编码,允许复用TCP连接。

消息1: HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello World
消息2: HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello

TCP在传输时,可能将这两个消息粘在一起

HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello WorldHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello

那么http要拆一下,拆包是指在TCP传输中,一个完整的HTTP请求或响应被分成了多个部分,接收方需要进行多次接收操作才能拼凑成一个完整的消息,大概长这样

部分1: HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello
部分2:  World

原因是因为TCP是字节流协议,没有消息的边界概念,而且不同的网络情况会导致数据发送和接收的时间也不同步。比方你买个沙发,沙发腿和其他组装部件可能不是一个快递包,有些配件比较快,其他的走物流比较慢,不可能同时到达。所以你自己拿到东西自己组合。但是我会给你发设计图,你可以判断是否缺少配件

在处理HTTP协议时,http通过某些方法来检测消息的边界,从而避免粘包和拆包问题。常见的解决办法包括:

  1. 基于固定长度的分隔符:HTTP协议使用了特定的分隔符,比如头部与主体之间使用\r\n\r\n作为分隔符。可以通过解析这些分隔符来确定消息的边界。
  2. Content-Length 头部:HTTP使用Content-Length头部来表示消息主体的长度,接收方可以根据这个头部来确定什么时候完整的消息接收完毕。
  3. Chunked Transfer Encoding:对于长连接或者流式数据传输时,使用HTTP的分块传输编码。每个块有独立的大小和边界标记。

白话文就是,标记好,长度的比较长度,3-1,3-2,3-3,大小的标记大小,100m-25m 100m-50m 100m-25m 或者通过特定的xxx公司xxxx有后续,xxxx公司xxx2无后续,这样去判断。

6. 泛型问题-泛型原理

编译时检查:使用泛型时,类型检查发生在 编译阶段。这意味着编译器会确保我们在使用泛型类或方法时,提供的类型参数是合法的。
举个常用的例子

List<String>只能持有String类型的对象。

类型擦除:Java泛型的底层实现使用了一种机制,称为类型擦除(Type Erasure),举个例子

在编译时,泛型类型的信息并不会在运行时保留,而是被擦除为其边界类型(例如,Object)。这意味着:

List<String>List<Integer>会在编译后都被转换为List<Object>

编译器会通过类型参数的边界信息(如果定义了边界)来进行相应的检查。比如,

如果定义为<T extends Number>,那么擦除后,T会被替换为Number

参数化类型:泛型可以用于类、接口和方法中的类型参数。例如:


   public class Box<T> {
       private T content;

       public void setContent(T content) {
           this.content = content;
       }

       public T getContent() {
           return content;
       }
   }

通配符:
Java泛型还引入了通配符的概念,用来代表不确定的类型。常见的通配符有:

?:代表一个不确定的类型。
? extends T:代表类型T的某个子类。
? super T:代表类型T的某个父类。

泛型的优点

类型安全:在编译时确保类型匹配,避免了运行时出现ClassCastException。
代码重用:可以编写 重点:(通用算法和数据结构,适用于多种不同类型)
更简洁的代码:泛型允许更清晰明了的代码,重点(使开发者能够少写或不写强制类型转换)

泛型的限制

不能创建泛型的实例(如:new T())。
不能使用静态泛型变量。
不能在泛型中使用基本类型(如:int、char),只能使用它们的包装类(如:Integer、Character)。
不支持结构性复合(如:T[],需要使用ArrayList等替代方法)。

7. MySQL 相关的三表联查(JOIN)问题

在这里插入图片描述

基本的三表联查

举个例子有三个表:users(用户表)、orders(订单表)和 products(产品表)。请写出查询所有用户及他们的订单和订单中的产品信息的 SQL 语句

SELECT 
    u.id AS user_id,
    u.name AS user_name,
    o.id AS order_id,
    o.order_date,
    p.id AS product_id,
    p.product_name
FROM 
    users u
JOIN 
    orders o ON u.id = o.user_id
JOIN 
    products p ON o.product_id = p.id;

继续,同样的三个表,但这次需要查询所有用户的信息,即使他们没有下过订单。

SELECT 
    u.id AS user_id,
    u.name AS user_name,
    o.id AS order_id,
    p.id AS product_id,
    p.product_name
FROM 
    users u
LEFT JOIN 
    orders o ON u.id = o.user_id
LEFT JOIN 
    products p ON o.product_id = p.id;


8.mysql mvcc核心

MySQL中的多版本并发控制(MVCC,Multi-Version Concurrency Control)是InnoDB存储引擎用来提高数据库并发性能的一种技术,它允许在执行读操作时不加锁,从而提高了并发性能。它通过保存数据在某个时间点的快照来实现非锁定读,这样就可以避免在读取数据时对写操作造成影响

  1. 隐藏列:

InnoDB为每行数据自动添加了三个隐藏的字段来支持MVCC:
DB_TRX_ID:最后修改该行的事务ID。
DB_ROLL_PTR:回滚指针,指向这个记录的上一个版本的位置。
DB_ROW_ID:行ID,如果没有定义主键,InnoDB会使用它来生成聚簇索引。

  1. Read View:

当事务读取数据时,InnoDB会为该事务创建一个称为“read view”的结构,这个结构定义了当前事务可以“看到”哪些数据。
read view中包含的信息主要是:
m_ids:当前系统中活跃的事务ID列表。
min_trx_id:当前系统中活跃的最小事务ID。
max_trx_id:下一个将被分配的事务ID。
creator_trx_id:创建这个read view的事务ID。
根据read view,事务可以确定每一行数据的可见性。如果某行的DB_TRX_ID在min_trx_id和max_trx_id之间,且不在m_ids列表中,该行对当前事务可见。

  1. Undo日志:

当数据被修改时,InnoDB会将修改前的数据存储在Undo日志中。如果一个事务需要看到旧的数据版本,InnoDB可以使用Undo日志来恢复早期版本的数据。
DB_ROLL_PTR将指向相关的Undo日志记录,这样可以链式后退到更早的数据版本。

  1. 快照读(Snapshot Read):

读取的是记录的可见版本(可能是历史版本),不更新数据。
例如:SELECT 操作。

  1. 当前读(Current Read):

读取的是记录的最新版本,并且会对读取的数据加锁。
例如:SELECT … FOR UPDATE 或 UPDATE 操作。

mysql事务概念篇

事务隔离级别

9. 红黑树和AVL树有什么区别?

红黑树和AVL树都是自平衡的二叉查找树,它们都保证了在插入和删除操作后,树的高度仍然保持在对数级别,从而获得高效的查找性能。它们的主要区别在于对平衡的定义和插入、删除操作的旋转复杂性:

  1. 平衡程度:

AVL树是严格平衡的二叉查找树,它要求每个节点的左子树和右子树的高度差最多为1。因此,AVL树的平衡程度高于红黑树,查找效率更高。
红黑树相对松散一些,它只要求从任一节点到叶子节点的所有路径中,黑节点数目必须相同。因此,在插入和删除时,红黑树的调整比AVL树要少。

  1. 插入和删除操作:

AVL树由于要求严格的平衡,插入和删除节点时可能需要多次旋转来保持平衡,相对复杂。
红黑树插入和删除节点时,由于平衡条件相对宽松,只需要少量固定次数的旋转即可完成平衡,对于插入和删除操作更友好。

  1. 应用场景:

AVL树由于查找效率高,适用于查找操作比较多的情况。
红黑树由于插入和删除效率高,且实现相对简单,适用于插入、删除和查找操作都比较多的情况。

10. juc包

  1. Executors
    使用场景:当你需要管理多个线程时,比如在服务器中处理多个用户请求。
    类:ExecutorService, ScheduledExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
    System.out.println("Running in a thread pool");
});
executor.shutdown();
  1. Locks
    使用场景:在复杂的同步需求中,标准的synchronized方法或代码块可能不够灵活,Lock接口提供了更复杂的锁操作。
    类:ReentrantLock, ReadWriteLock, StampedLock
Lock lock = new ReentrantLock();
lock.lock();
try {
    // critical section code
} finally {
    lock.unlock();
}
  1. Atomic Variables
    使用场景:用于在无锁环境中进行线程安全的操作,通常用于计数器或累加器。
    类:AtomicInteger, AtomicLong, AtomicReference
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
  1. Concurrent Collections
    使用场景:在多线程环境下安全高效地执行集合操作。
    类:ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue
Map<String, String> map = new ConcurrentHashMap<>();
map.put("key", "value");
  1. CountDownLatch
    使用场景:允许一个或多个线程等待其他线程完成操作。
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
    latch.countDown();
}).start();
latch.await();
  1. CyclicBarrier
    使用场景:使一组线程到达一个同步点时被阻塞,直到最后一个线程到达,所有被阻塞的线程才能继续执行。
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Barrier action"));
Runnable task = () -> {
    // do something
    barrier.await();
};
new Thread(task).start();
  1. Semaphore
    使用场景:控制对某组资源的访问权限。
Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
try {
    // access the resource
} finally {
    semaphore.release();
}

在 Java 8 中新增的 CompletableFuture 类是 java.util.concurrent 包的一部分,它实现了 Future 接口并提供了更丰富的异步编程能力。

  1. supplyAsync:异步执行任务,并返回一个新的 CompletableFuture,该 CompletableFuture 会在任务完成时获得结果。常用的几个方法了解
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 执行长时间的任务
    return "Task Result";
});
  1. thenApply:当 CompletableFuture 的计算结果完成,对计算结果应用函数。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(s -> s + " World");
  1. thenCombine:将两个独立的 CompletableFuture 合并为一个新的 CompletableFuture。
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " World");
CompletableFuture<String> result = future1.thenCombine(future2, String::concat);

11.rabbitMq ack机制

rabbit基础知识

rabbitMq ack机制

12. Redission redis 分布式锁解决方案

Redission 拆包

13. redis zset跳表

跳表概念

14. java锁概念 篇

锁概念基本知识

15.hashMap 原理

hashMap调研

hashMap和Hashtable区别

白话文来说,Hashtable是Java中的一个线程安全的哈希表实现,它和HashMap类似,也是基于数组和链表的数据结构,但是Hashtable不允许null键或null值,Hashtable同样使用哈希函数来将键映射到数组索引,为解决哈希冲突,Hashtable同样使用链表存储具有相同哈希值的项,Hashtable的每个方法都是同步的,使用synchronized关键字保证线程安全,这意味着多个线程可以安全地共享单个Hashtable实例,Hashtable在达到一定填充度时也会扩容,但扩容过程中会阻塞其他线程对它的操作,因为方法是同步的。
由于Hashtable的线程安全特性是通过锁整个哈希表来实现的,这导致了效率问题。因此,在不需要线程安全的场景下,推荐使用HashMap;如果确实需要线程安全,可以考虑使用Collections.synchronizedMap(new HashMap<…>())或者ConcurrentHashMap,值得补充的是jd8 Hashtable已经废弃了

16. Spring Boot 自动化装配原理

自动化装配源码

17. hbase概念篇 (大数据项目相关的看一下)

hbase 概念篇

18 . k8s 入门篇(运维不是必须项,架构要求的看一下)

k3s 概念篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值