在40岁老架构师尼恩的(50+)读者社群中,经常有小伙伴,需要面试美团、京东、阿里、 百度、头条等大厂。
下面是一个小伙伴成功拿到通过了美团一面面试,现在把面试真题和参考答案收入咱们的宝典。
通过美团一面真题, 大家可以看看,收个优质Offer需要学点啥?
总之,光代码漂亮不够, 面试,还得会吹。
这里把题目以及答案,经过整理和梳理之后,收入咱们的《尼恩Java面试宝典PDF》 V121版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发、吹牛水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取
要实现共同关注功能(如社交网络中的好友关系),可以利用 Redis 数据结构来存储关注关系。
给个示例:
假设存在两个用户,用户 A 和用户 B,他们都可以关注其他用户。我们希望找到他们共同关注的用户。
用户A的关注列表:sadd userA_following userC userD userE
用户B的关注列表:sadd userB_following userD userE userF
使用集合的交集操作(sinter
)找出两个用户的共同关注:
sinter userA_following userB_following
该操作将返回一个集合,包含用户 A 和用户 B 共同关注的用户(在此示例中是 userD 和 userE)。
redis 支持 Set集合的数据存储,其中有三个比较特殊的方法:
sinter key [key …]
查看一个集合的全部成员,该集合是所有给定集合的交集。sunion key [key …]
查看一个集合的全部成员,该集合是所有给定集合的并集。sdiff key [key …]
查看所有给定 key 与第一个 key 的差集redis> SMEMBERS group_1
1) "LI LEI"
2) "TOM"
3) "JACK"
redis> SMEMBERS group_2
1) "HAN MEIMEI"
2) "JACK"
redis> SINTER group_1 group_2 # 取的是交集的数据
1) "JACK"
redis> SMEMBERS songs
1) "Billie Jean"
redis> SMEMBERS my_songs
1) "Believe Me"
redis> SUNION songs my_songs # 取的是集合的并集数据
1) "Billie Jean"
2) "Believe Me"
redis> SMEMBERS peter_movies
1) "bet man"
2) "start war"
3) "2012"
redis> SMEMBERS joe_movies
1) "hi, lady"
2) "Fast Five"
3) "2012"
redis> SDIFF peter_movies joe_movies # 取的是两个集合的差集
1) "bet man"
2) "start war"
两种持久化策略:
a. AOF持久化以追加的方式记录每个写操作(包括SET、INCR等)到一个日志文件中,该文件包含了恢复数据所需的所有写操作。
b. AOF持久化可以配置为每秒同步一次(默认配置),或者根据需要更频繁地同步。
c. 由于AOF记录了每个写操作,通常情况下不会丢失数据,即使Redis宕机,也可以通过AOF文件来完全恢复数据。
a. RDB持久化是通过周期性快照(快照)整个数据集到磁盘的方式。
b. RDB 文件包含特定时间点数据集的快照,因此两次快照之间的数据更改可能会丢失。
c. 默认情况下,Redis 每隔一段时间(可配置)执行一次 RDB 快照。
若 Redis 在两次快照间崩溃,可能导致数据丢失。
总的来说:
在 redis.conf 配置文件的 APPEND ONLY MODE 下:
①、appendonly:默认值为no,也就是说redis 默认使用的是rdb方式持久化,如果想要开启 AOF 持久化方式,需要将 appendonly 修改为 yes。 AOF 保存文件的位置和 RDB 保存文件的位置一样,都是通过 redis.conf 配置文件的 dir 配置文件的位置.
②、appendfilename :aof文件名,默认是"appendonly.aof"
③、**appendfsync:**aof持久化策略的配置;
④、no-appendfsync-on-rewrite:在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。默认值为no。
⑤、auto-aof-rewrite-percentage:默认值为100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
⑥、auto-aof-rewrite-min-size:64mb。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。
⑦、aof-load-truncated:aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象 redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为 yes。
优点:
①、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。
②、AOF 文件使用 Redis 命令追加的形式来构造,因此,即使 Redis 只能向 AOF 文件写入命令的片断,使用 redis-check-aof 工具也很容易修正 AOF 文件。
③、AOF 文件的格式可读性较强,这也为使用者提供了更灵活的处理方式。例如,如果我们不小心错用了 FLUSHALL 命令,在重写还没进行时,我们可以手工将最后的 FLUSHALL 命令去掉,然后再使用 AOF 来恢复数据。
缺点:
①、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大。
②、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。但在 Redis 的负载较高时,RDB 比 AOF 具好更好的性能保证。
③、RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 方式更健壮。官方文档也指出,AOF 的确也存在一些 BUG,这些 BUG 在 RDB 没有存在。
那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?
如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,而且使用 RDB 还可以避免 AOF 一些隐藏的 bug;否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。Redis后期官方可能都有将两种持久化方式整合为一种持久化模型。
在Redis4.0之后,在RDB和AOF两种持久化方式之外,又新增了RDB-AOF混合持久化方式。
这种方式结合了RDB和AOF的优点,既能快速加载又能避免丢失过多的数据。
具体配置为:
aof-use-rdb-preamble
设置为yes表示开启,设置为no表示禁用。
当开启混合持久化时,主进程先fork出子进程将现有内存副本全量以RDB方式写入aof文件中,然后将缓冲区中的增量命令以AOF方式写入aof文件中,写入完成后通知主进程更新相关信息,并将新的含有 RDB和AOF两种格式的aof文件替换旧的aof文件。
简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。
这种方式优点我们很好理解,缺点就是不能兼容Redis4.0之前版本的备份文件了。
a. Bloom Filter:利用布隆过滤器筛选出不在缓存中的请求,以降低对数据库的请求压力。
b. 空值缓存:虽数据库中不存在该数据,但仍将其缓存,但设置较短的过期时间,以防频繁查询。
c. 缓存空对象:当数据库查询结果为空时,也将该结果缓存,但设定较短的过期时间,避免重复查询。
a. 互斥锁:采用互斥锁保护缓存,当缓存失效时,只允许一个请求查询数据库,其他请求等待结果,避免多个请求同时击穿缓存。
b. 热点数据预热:定期或启动时预先加载热门数据至缓存,防止因突发大量请求导致缓存击穿。
a. 缓存失效时间随机性:为缓存失效时间增加一定随机性,使缓存不会同时失效,减轻对数据库的并发请求压力。
b. 持久化缓存:利用 AOF 和 RDB 的持久化机制确保缓存数据可靠性,即使在缓存雪崩情况下,也能从持久化数据中恢复。
c. 多级缓存:采用多级缓存(如本地内存缓存、分布式缓存、CDN 等)分担缓存压力,使某一缓存层发生雪崩时,其他层仍能提供服务。
a. 异步刷新:缓存失效后,后台异步更新缓存,避免请求等待缓存更新。
b. 加锁更新:缓存失效时,仅允许一个请求查询数据库并更新缓存,其他请求等待结果。
a. 根据数据访问模式和业务需求,选择合适的缓存策略,如 LRU(最近最少使用)、LFU(最不常使用)、TTL(Time To Live)等。
Redis中的大Key通常指缓存键对应的值较大,可能包含大型数据结构、大量文本或二进制数据等。这类数据会导致内存占用过高,进而影响性能。给大家一些处理Redis大Key的参考方法:
a. 如有条件,将大型数据拆分成多个小型数据,并分别存储在独立的键中。这能降低单个键的大小,减少内存占用。
b. 例如,若某个键存储了大型 JSON 对象,可将其拆分为多个子键,每个子键存储 JSON 对象的部分数据。
a. 在存储文本或二进制数据之前,可对其进行压缩,在读取时再解压缩。虽然 Redis 本身不支持数据压缩,但可在应用层实现压缩和解压缩操作。
a大 Key 是由多个小 Key 组成的集合,可以使用 Redis 的数据分片或分区技术,将数据分布在多个 Redis 实例中,每个实例负责一部分数据。
这有助于减轻单个实例的内存压力。
a. 若大 Key 生命周期有限,可以定期清理不再需要的数据,以释放内存。
b. 使用DEL
命令删除不再需要的大Key。
对于大型集合或列表,可以考虑使用 Redis 的内存优化数据结构,如 HyperLogLog、Redis Streams 等,以降低内存占用。
如果大 Key 是在系统启动时加载的,可以实施数据预热策略,提前将热门数据加载到缓存中,以减轻启动时的内存压力。
给大家一些选择是需要考虑的因素,在什么情况用哪种分布式锁是更优选择。
a. 通常情况下,Redis 的性能优于 ZooKeeper,因为它采用内存存储系统,而 ZooKeeper 采用磁盘存储。
b. 如果你追求高性能的分布式锁,Redis 是更好的选择。
a. ZooKeeper 提供强一致性,适合对一致性要求较高的分布式应用,如协调和选举等。
b. Redis 的分布式锁在某些情况下可能出现失效或死锁,因为它是基于主从复制的。
a. ZooKeeper 需要独立的 ZooKeeper 集群,需进行维护和管理。
b. Redis 部署相对简单,特别是若你在应用中已使用 Redis。
a. Redis 扩展和部署较容易,因此便于实现高可用性。
b. ZooKeeper 的部署和维护较为复杂,尤其在多数据中心环境中。
a. 若你在应用中已使用 Redis,添加 Redis 分布式锁较为容易集成。
b. 若应用已使用 ZooKeeper,选择 ZooKeeper 分布式锁更为合适。
a. 需更强数据持久性和一致性时,可考虑使用 ZooKeeper。
b. Redis 提供持久性,但通常在性能和可用性之间作出权衡。
Redis 和 ZooKeeper 均有活跃的社区支持,需根据具体需求评估社区支持和文档资源。
综合考虑以上因素,若应用需高性能分布式锁且可容忍一定程度的一致性弱点,可选 Redis 分布式锁。若需强一致性和更复杂分布式协同操作,ZooKeeper 分布式锁更适合。
在分布式系统中,熔断和限流是两种关键技术,它们有助于提高系统的可用性和稳定性。
熔断机制旨在防止系统出现雪崩效应。当某个服务或组件的错误率超过设定阈值,熔断器就会启动,阻止进一步的请求访问该服务,以避免更多系统部分崩溃。熔断器开启后,会定时检查服务可用性,一旦恢复正常,便关闭熔断器,允许请求再次访问。
熔断的好处包括:
1. 熔断器实现
import java.util.concurrent.TimeUnit;
public class CircuitBreaker {
private final ThreadPoolExecutor executor;
private final Semaphore permit;
private final long timeout;
public CircuitBreaker(int maxThreads, long timeout, TimeUnit unit) {
this.executor = new ThreadPoolExecutor(maxThreads, maxThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
this.permit = new Semaphore(1);
this.timeout = timeout;
}
public void execute(Runnable command) {
permit.acquire();
try {
executor.execute(command);
} catch (Exception e) {
// 处理异常,例如记录日志、发送告警等
System.err.println("Error occurred while executing command: " + e.getMessage());
} finally {
permit.release();
}
}
public void reset() {
executor.shutdown();
try {
if (!executor.awaitTermination(timeout, TimeUnit.MILLISECONDS)) {
// 超过恢复时间,仍然无法恢复,则考虑进行降级或熔断
System.err.println("Failed to reset circuit breaker");
}
} catch (InterruptedException e) {
// 等待过程中出现中断,表示恢复失败
System.err.println("Interrupted while waiting for circuit breaker reset: " + e.getMessage());
}
executor = new ThreadPoolExecutor(executor.getPoolSize(), executor.getPoolSize(), 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
}
}
2.使用示例
public class CircuitBreakerDemo {
public static void main(String[] args) {
CircuitBreaker circuitBreaker = new CircuitBreaker(10, 5000, TimeUnit.MILLISECONDS);
for (int i = 0; i < 10; i++) {
circuitBreaker.execute(() -> {
System.out.println("Executing command " + i);
throw new RuntimeException("模拟异常");
});
}
circuitBreaker.reset();
for (int i = 0; i < 10; i++) {
circuitBreaker.execute(() -> {
System.out.println("Executing command " + i);
});
}
}
}
限流机制旨在控制服务请求速率,防止系统同时承受过多请求。根据应用需求,限流可以有多种实现方式,如:
限流的好处包括:
1. 定义一个 TokenBucket 类:
import java.util.concurrent.TimeUnit;
public class TokenBucket {
private final long capacity;
private final long tokensPerSecond;
private long lastRefillTime;
private long tokens;
public TokenBucket(long capacity, long tokensPerSecond) {
this.capacity = capacity;
this.tokensPerSecond = tokensPerSecond;
this.lastRefillTime = System.currentTimeMillis();
this.tokens = capacity;
}
public boolean consume(long tokens) {
if (tokens > capacity) {
return false;
}
refill();
if (tokens <= tokens) {
tokens -= tokens;
return true;
} else {
return false;
}
}
private void refill() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
long newTokens = elapsed * tokensPerSecond;
tokens = Math.min(capacity, tokens + newTokens);
lastRefillTime = now;
}
}
2. 使用示例:
public class Main {
public static void main(String[] args) {
TokenBucket bucket = new TokenBucket(10, 2);
System.out.println(bucket.consume(5)); // 消耗 5 个令牌,返回 True
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bucket.consume(6)); // 消耗 6 个令牌,返回 False
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bucket.consume(2)); // 消耗 2 个令牌,返回 True
}
}
在这个例子中,我们创建了一个 TokenBucket 类,通过 consume 方法限制每秒请求的次数。每次调用 consume 方法时,会先检查当前令牌是否足够,如果不够,则返回 False,表示限流。如果足够,则消耗令牌并返回 True。在 refill 方法中,我们会根据时间间隔和每秒添加的令牌数来补充令牌。
这个简单的限流器可以用于保护 Java 系统的瓶颈部分,防止流量过大导致系统崩溃。在实际应用中,可以根据需求进行优化和扩展。
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringValueSerializer;
public class KafkaExample {
public static void main(String[] args) {
// 创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<>(
new StringValueSerializer(),
new StringValueSerializer()
);
// 创建消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(
new StringValueSerializer(),
new StringValueSerializer()
);
// 发送消息
producer.send("test-topic", "Hello, Kafka!");
// 接收消息
consumer.subscribe("test-topic");
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("Received message: %s%n", record.value());
}
}
}
}
综上所述,Kafka 实现高可用、高吞吐的主要手段包括:分布式架构、顺序读写、零拷贝、文件分段、批量发送和数据压缩等技术。通过这些技术和优化,Kafka 能够在大规模消息处理场景下表现出优异的性能。
Kafka 致力于实现不重不丢,这意味着它努力确保消息不会被重复传递,也不会在传递过程中丢失。为了实现这一目标,Kafka 采用了以下关键机制:
以下是一个简单的 Java 代码示例,实现了 Kafka 生产者和消费者功能,同时展示了如何确保消息不重复和不丢失。
生产者端(Producer.java):
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.Properties;
public class Producer {
public static void main(String[] args) {
// 配置生产者参数
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 创建生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 发送消息
for (int i = 0; i < 10; i++) {
String message = "Hello, Kafka!" + i;
producer.send(new ProducerRecord<>("test-topic", message));
System.out.printf("Sent message: %s%n", message);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 关闭生产者
producer.close();
}
}
这个生产者实例使用了 KafkaProducer 类,并发送了 10 条消息到名为 test-topic 的 topic。在 main 方法中,调用 send 方法发送消息,并在发送消息后打印消息内容。生产者在运行过程中,会持续发送消息。当需要结束程序时,调用 close 方法关闭生产者。
消费者端(Consumer.java):
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class Consumer {
public static void main(String[] args) {
// 配置消费者参数
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
// 创建消费者实例
Consumer<String, String> consumer = new KafkaConsumer<>(props);
// 订阅 topic
consumer.subscribe(Collections.singletonList("test-topic"));
// 消费消息
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
// 关闭消费者
consumer.close();
}
}
这个消费者实例使用了 KafkaConsumer 类,并订阅了名为 test-topic 的 topic。在 main 方法中,使用 poll 方法轮询消息,并在接收到消息时打印 offset、key 和 value。消费者在运行过程中,会持续接收和处理消息。当需要结束程序时,调用 close 方法关闭消费者。
Kafka 消费者端的幂等性是指消费者能够处理来自 Kafka 主题的消息,而不会导致重复数据或意外的结果。保持幂等性对于确保数据处理的正确性和稳定性至关重要。
以下是一些建议和方法,用于实现 Kafka 消费者端的幂等性:
以下是一个简单的 Java 示例,展示了如何实现 Kafka 消费端的幂等性:
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class KafkaConsumerIdempotenceDemo {
public static void main(String[] args) {
// 配置消费者参数
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("auto.offset.reset", "earliest");
// 创建消费者实例
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 订阅 topic
consumer.subscribe(Collections.singletonList("test-topic"));
// 消费消息
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 处理消息
processMessage(record.value());
// 提交偏移量
consumer.commit(record.offset());
}
}
// 关闭消费者
consumer.close();
}
private static void processMessage(String message) {
// 实现幂等处理逻辑,例如使用数据库事务或悲观锁
// 这里仅作为示例,模拟数据库操作
System.out.println("Processing message: " + message);
}
}
在这个示例中,我们创建了一个 Kafka 消费者,订阅了名为 test-topic 的 topic。在 poll 方法中获取消息,并对每条消息进行幂等处理。处理完成后,使用 commit 方法提交消费偏移量。
需要注意的是,这个示例仅实现了消费端的幂等性,并未涉及生产端的消息幂等。
在实际应用中,为了确保消息的幂等性,需要在生产端和消费端都进行相应的处理。此外,Kafka 的幂等性仅保证了单个分区内的消息不重复,不同分区之间仍有可能出现重复消息。如需保证多个分区的幂等性,可以考虑使用 Kafka 的事务功能。
Kafka 消费者群组是 Kafka 中用于协同消费主题中消息的机制。它允许多个消费者协同工作,以从一个或多个主题中消费消息。
Kafka 生产端的主要职责是将消息发送至 Kafka 集群,并确保消息被正确路由至适宜的主题和分区。在这一过程中,Kafka 采用了分区策略来对消息进行分区,从而实现消息在各个分区间的均衡分布。
以下是一些相关概念:
Kafka 生产端(Producer)在发送消息时,需要考虑消息的分发策略。Kafka 生产端通过路由来将消息发送到与主题(Topic)关联的分区(Partition),从而实现高效的消息处理和数据分布。以下是一个使用 Java 实现 Kafka 生产端的路由和分配的示例:
1. 添加 Maven 依赖:
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-clientsartifactId>
<version>2.8.0version>
dependency>
2. 创建 Kafka 生产者并配置参数:
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaProducerDemo {
public static void main(String[] args) {
// 配置生产者参数
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建 Kafka 生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 发送消息
for (int i = 0; i < 10; i++) {
String message = "Hello, Kafka!" + i;
producer.send(new KeyValue<>(message, message));
}
// 关闭生产者
producer.close();
}
}
在上面的示例中,我们创建了一个 Kafka 生产者,并配置了 bootstrap.servers、key.serializer 和 value.serializer 等参数。生产者将消息发送到名为"test-topic"的主题,该主题有两个分区(Partition)。
3. 实现路由和分配:
Kafka 生产端通过路由和分配策略来决定将消息发送到哪个分区。这可以通过实现自定义的 Partitioner 类来实现。以下是一个简单的示例,将消息发送到分区序号小于等于消息序号的分区:
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.requests.SendResult;
import java.util.List;
public class CustomPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, int numPartitions) {
int partition = (int) (key.hashCode() % numPartitions);
if (partition < 0) {
partition = 0;
}
return partition;
}
@Override
public void send(SendResult sendResult, List<Object> records) {
// 实现自定义发送逻辑,例如记录发送结果
}
}
4. 使用自定义分区器发送消息:
在之前的生产者示例中,我们将 Partitioner 类替换为自定义的 CustomPartitioner。然后,将消息发送到与主题关联的分区:
// 创建自定义分区器
CustomPartitioner partitioner = new CustomPartitioner();
// 发送消息
for (int i = 0; i < 10; i++) {
String message = "Hello, Kafka!" + i;
SendResult sendResult = producer.send(new KeyValue<>(message, message), partitioner);
System.out.printf("Message %s sent to partition %d with offset %d%n", message, sendResult.getPartition(), sendResult.getOffset());
}
a. 这是一种用于唯一标识每条记录的索引。每个表格仅能设立一个主键索引。
b. 主键索引通常用于加速检索特定记录或进行数据修改操作。
c. 主键索引要求索引列的值唯一且非空。
d. 主键索引作为表的聚集索引,意味着数据按照主键索引的顺序进行物理存储。
e. 主键索引通常具有较高的查找效能,因为它能快速定位到特定行。
a. 二级索引是除主键索引以外的一种索引,用于加速特定查询条件的查找。
b. 表可以有多个二级索引,用于加速不同类型的查询,如根据非主键列的条件检索数据。
c. 二级索引的值可以重复,不要求唯一性,因为它们是辅助索引,主要用于快速定位主键值。
d. 二级索引通常包含索引列的值和对应的主键值,以便在查找时可直接找到对应行。
e. 二级索引能提升查询性能,但也会占用额外的存储空间和增加更新操作的开销。
原子性(Atomicity):
a. 原子性确保事务是不可分割的操作单元,要么全部执行,要么全部不执行。若事务的任何部分失败,整个事务将回滚,数据库状态恢复至初始状态。
b. 原子性旨在防止不完整或部分执行的事务,确保数据库一致性。
一致性(Consistency):
a. 一致性确保事务在执行前后保持数据库的一致性状态。即事务执行后,数据库应从一个一致状态转变为另一个一致状态。
b. 一致性要求事务操作遵循数据库的完整性约束和业务规则,维持数据合法性。
隔离性(Isolation):
a. 隔离性确保并发执行的事务不会相互干扰,每个事务都仿佛在无其他事务干扰的情况下执行。
b. 隔离性分为不同级别(如读未提交、读已提交、可重复读和串行化),以控制并发事务间的相互影响。
持久性(Durability):
a. 持久性确保事务成功提交后,其结果永久存储在数据库中,即使系统崩溃或断电也不会丢失。
b. 数据库系统通常采用日志文件实现持久性,以便在系统崩溃后恢复事务。
幻读,又称不可重复读,是一种并发事务问题。它发生在多个事务之间,其中一个事务在某个范围内插入新行,而另一个事务在此范围内尝试查询数据。这可能导致查询事务看到新插入的行,即使在其开始查询之前这些行并不存在。幻读与脏读类似,但关注的是插入操作而非修改操作。
RR 隔离级别通过使用锁或多版本并发控制(MVCC)来解决幻读问题。在 RR 隔离级别下,事务会获取一个范围锁,确保在事务进行中查询的范围内的数据在事务结束前不会被其他事务修改或插入。
例如,如果事务 A 在 RR 隔离级别下查询某个范围的数据,另一个事务 B 想要在相同范围内插入新行,事务 B 将被阻塞,直到事务 A 完成。这样可以防止事务 A 看到事务 B 插入的新行,从而解决幻读问题。
假设一个在线购物系统,多个用户同时浏览某个商品的库存情况。如果一个用户正在查询库存时,另一个用户刚好购买了最后一件商品,事务A可能在查询时看到商品的数量是1,但在实际购买时,库存已经为0了,这就是幻读问题。
MVCC(多版本并发控制)是一种并发控制机制,允许数据库系统在同一时间点存在多个版本的数据,以支持事务隔离和并发查询。每个数据行在 MVCC 中都有一个或多个版本号,用于标识数据的不同版本。版本号的生成和存储取决于数据库管理系统的具体实现。
通常,MVCC系统中的版本号是在数据行上生成的,并且通常包括以下信息:
版本号的生成和存储方式因 DBMS 而异,但通常存储在数据行的元数据中,以便系统能够在查询时识别和访问不同版本的数据。数据库系统还维护一个版本控制的数据结构,通常称为版本链或版本表,以跟踪每个数据行的不同版本及其关系。
MVCC 的主要优点是它允许高度并发的读取操作,因为每个事务都可以看到一致性的数据快照,而不会阻塞其他事务的写入操作。不同数据库管理系统的 MVCC 实现方式可能有所不同,但它们都旨在提供高并发性和事务隔离。在查询时,数据库系统会根据当前事务的 ID 或时间戳选择适当版本的数据,以确保事务之间的隔离。
a. binlog
是 MySQL 数据库中的二进制日志,用于记录数据库的变更操作,如插入、更新和删除。它以二进制形式记录了 SQL 语句或数据变动事件的日志,而不是实际的数据值。
b. 主要用途在于数据库的备份、主从复制和故障恢复。通过分析 binlog
,可以还原数据库的历史状态。
a. redo log
是数据库管理系统中的一种日志,主要用于记录数据变动操作。它以物理方式记录了对数据库页的更改,而非 SQL 语句。
b. 主要用途是确保事务的持久性(Durability),在数据库系统发生崩溃或故障时,可以使用 redo log
来重放事务,以确保数据的一致性。
a. undo log
也是数据库管理系统中的一种日志,用于记录事务的撤销操作。它包含了事务执行前的数据状态,以便在需要时回滚事务。
b. 主要用途是支持事务的回滚操作和多版本并发控制(MVCC)。在事务发生回滚时,可以使用 undo log
将数据还原到之前的状态。
简单记忆:
binlog
用于记录数据更改的逻辑日志,通常用于备份和复制。redo log
用于记录物理数据页的更改,以确保持久性。undo log
用于支持事务的回滚和多版本并发控制。当数据库系统发生崩溃或非正常关闭时,崩溃恢复日志(通常是 redo log
)用于重放未完成的事务,以确保数据的持久性。主从同步通常依赖于二进制日志(Binary log)来保持主数据库和从数据库之间的数据一致性,以支持数据库复制和高可用性方案。
数据库管理中存在多种不同类型的日志,各自用于不同的目的,主要包括:
1. Crash Recovery Log(崩溃恢复日志):
redo log
,用于重放未完成的事务以确保数据的持久性。2. Binary Log(二进制日志或Binlog):
3. Error Log(错误日志):
4. Transaction Log(事务日志):
undo log
,它用于支持事务的回滚操作。两阶段提交(Two-Phase Commit)是分布式系统中的一种事务协议,用于在多个节点上执行原子操作。
在两阶段提交中,事务分为两个阶段:预提交(Pre-Commit)和确认提交(Commit)。
以下是对两阶段提交的具体解释和 Java 实现。
在这个阶段,事务需要在所有参与者节点上执行,并将执行结果存储在事务日志(如 MySQL 的 binlog)中。此时,事务仍然可以被回滚(Rollback)。
在这个阶段,事务已经完成在所有参与者节点上的执行,并且日志已经被持久化。此时,事务不能被回滚,只能向前推进(Commit)或回滚(Rollback)。
以下是 Java 实现的两阶段提交代码示例:
public class TwoPhaseCommit {
private final Logger logger = LoggerFactory.getLogger(TwoPhaseCommit.class);
private final AtomicBoolean committed = new AtomicBoolean(false);
public void preCommit() {
logger.info("Entering pre-commit stage");
// 在这里执行事务操作,如更新、插入等
// ...
logger.info("Finished pre-commit stage");
}
public void commit() {
if (committed.getAndUpdate(true, x -> true)) {
logger.info("Entering commit stage");
// 在这里执行提交操作,如持久化日志、发送确认消息等
// ...
logger.info("Finished commit stage");
} else {
logger.warn("Commit failed");
}
}
public void rollback() {
if (committed.getAndUpdate(false, x -> false)) {
logger.info("Entering rollback stage");
// 在这里执行回滚操作,如撤销更新、删除日志等
// ...
logger.info("Finished rollback stage");
} else {
logger.warn("Rollback failed");
}
}
}
在这个示例中,我们使用了一个原子布尔变量 committed
来标记事务是否已经进入确认提交阶段。
在预提交阶段,事务执行操作并将结果记录在日志中。
然后,事务进入确认提交阶段,执行持久化操作并发送确认消息。如果事务已经进入确认提交阶段,那么不能再回滚事务。
使用这个两阶段提交框架,可以确保事务在所有节点上的一致性和原子性。在实际应用中,还需要考虑如何处理事务日志、异常处理、并发控制等问题。
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择某一天买入这只股票,并在未来的某一天卖出。
设计一个算法来计算你所能获得的最大利润。
返回你从这笔交易中能获得的最大利润。如果无法获得任何利润,返回 0
。
以下是 Java 代码实现:
public class Main {
public static void main(String[] args) {
int[] prices = {7, 1, 5, 3, 6, 4};
System.out.println(maxProfit(prices));
}
public static int maxProfit(int[] prices) {
int buy = Integer.MIN_VALUE, sell = Integer.MIN_VALUE;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
int temp = Math.max(buy, prices[i] - sell);
buy = Math.max(buy, temp);
sell = Math.min(sell, prices[i] - temp);
maxProfit = Math.max(maxProfit, sell - buy);
}
return maxProfit;
}
}
当输入 [7, 1, 5, 3, 6, 4]
时,输出结果为 5
,表示从这笔交易中能获得的最大利润为 5。
在尼恩的(50+)读者社群中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用多篇文章,给大家介绍阿里、百度、字节、滴滴的真题:
《炸裂,靠“吹牛”过京东一面,月薪40k》
《太猛了,靠“吹牛”过顺丰一面,月薪30K》
《问懵了…美团一面索命44问,过了就60W+》
《炸裂了…京东一面索命40问,过了就50W+》
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文收录于 《尼恩Java面试宝典》。
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易拿到滴。另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓