RabbitMQ

服务异步调用:服务A如何保证异步请求一定能被服务B就收到并处理

削峰:海量请求,如何实现削峰效果,将请求全部放到一个队列中 慢慢消费 这个队列怎么实现?
服务解耦:如何尽量降低服务之间的耦合问题,如果在订单服务与积分服务解耦 需要一个队列 而这个队列依然需要实现上述两种功能
前提系统安装了 docker和docker-compose
https://m.runoob.com/docker/docker-compose.html
version: "3.1" services: rabbitmq: image: daocloud.io/library/rabbitmq:3.8.5 container_name: rabbitmq restart: always volumes: - ./data/:/var/lib/rabbitmq/ ports: - 5672:5672 - 15672:15672
执行
curl localhost:5672
默认图形化界面是关闭的
docker exec -it rabbitmq bash
cd /opt/rabbitmq/
cd plugins/
cd ../sbin/
开启图形化界面 ./rabbitmq-plugins enable rabbitmq_management
在window访问
http://192.168.1.137:15672/
账号密码为
guest
"Hello World!" 一个队列被一个消费者消费 入门
Work queues 一个队列被多个消费者消费(如果 消费者的消费速率不一致 但是消费量却一样 就会导致 消费快的空转 而消费慢的消息迟迟消化不完生产者生产的消息)
此时我们需要关闭自动ack 手动去提交ack
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);

Publish/Subscribe 手动创建Exchange(FANOUT) 需要自定义创建一个交换机

Routing 手动创建Exchange(DIRECT)

Topics 手动创建Exchange(TOPIC)

RPC RPC方式

Publisher Confirms
构建Connection工具类
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
@SpringBootTest class MallOrderApplicationTests { public static final String RABBITMQ_HOST="192.168.1.137"; public static final Integer RABBITMQ_PORT=5672; public static final String RABBITMQ_USERNAME="guest"; public static final String RABBITMQ_PASSWORD="guest"; public static final String RABBITMQ_VIRTUAL_HOST="/"; public static final String QUEUE_NAME="hello"; //构建rabbitMQ链接对象 public static Connection getConnection() throws IOException, TimeoutException { //创建connection工厂 ConnectionFactory factory = new ConnectionFactory(); //设置RabbitMq 连接信息 factory.setHost(RABBITMQ_HOST); factory.setPort(RABBITMQ_PORT); factory.setUsername(RABBITMQ_USERNAME); factory.setPassword(RABBITMQ_PASSWORD); factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST); //返回链接对象 Connection connection = factory.newConnection(); return connection; } @Test public void publish() throws IOException, TimeoutException { //获取连接对象 Connection connection = MallOrderApplicationTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 durable 队列是否持久化 exclusive 如果为true 只允许一个消费者消费 autoDelete 队列长时间没有使用 自动删除 channel.queueDeclare(QUEUE_NAME, false, false, false, null); String message="Hello World"; //发布消息 使用rabbitMQ自带的默认交换机 routingkey 叫什么 就把消息路由到哪个队列中 channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println("消息发送成功"); System.in.read(); } @Test public void consumer() throws Exception { //获取连接对象 Connection connection = MallOrderApplicationTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body) ); } }; channel.basicConsume(QUEUE_NAME, true, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } }
Work queues 实现代码

@SpringBootTest class WorkqueuesTests { public static final String RABBITMQ_HOST="192.168.1.137"; public static final Integer RABBITMQ_PORT=5672; public static final String RABBITMQ_USERNAME="guest"; public static final String RABBITMQ_PASSWORD="guest"; public static final String RABBITMQ_VIRTUAL_HOST="/"; public static final String QUEUE_NAME="work"; //构建rabbitMQ链接对象 public static Connection getConnection() throws IOException, TimeoutException { //创建connection工厂 ConnectionFactory factory = new ConnectionFactory(); //设置RabbitMq 连接信息 factory.setHost(RABBITMQ_HOST); factory.setPort(RABBITMQ_PORT); factory.setUsername(RABBITMQ_USERNAME); factory.setPassword(RABBITMQ_PASSWORD); factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST); //返回链接对象 Connection connection = factory.newConnection(); return connection; } @Test public void publish() throws IOException, TimeoutException { //获取连接对象 Connection connection = WorkqueuesTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 durable 队列是否持久化 exclusive 如果为true 只允许一个消费者消费 autoDelete 队列长时间没有使用 自动删除 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //发布消息 使用rabbitMQ自带的默认交换机 routingkey 叫什么 就把消息路由到哪个队列中 for (int i = 0; i < 100; i++) { String message="Hello World"+i; channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); } System.out.println("消息发送成功"); System.in.read(); } @Test public void consumer1() throws Exception { //获取连接对象 Connection connection = WorkqueuesTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //设置消息的流控 一次只消费一个 别一次性分发完后 让consumer自己慢慢消费 此设置是让consume消费完一条消息后再从channel中再获取下一条消息 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者一号获取到的消息:"+new String(body) ); channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } @Test public void consumer2() throws Exception { //获取连接对象 Connection connection = WorkqueuesTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //设置消息的流控 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者二号获取到的消息:"+new String(body) ); //deliveryTag 标识 channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } }
Publish/Subscribe 需要自定义交换价FANOUT 就是不关注routing key 是什么 只要直接绑定了交换机 就发送消息给绑定的queue
@SpringBootTest class PublishSubscribeTests { public static final String RABBITMQ_HOST="192.168.1.137"; public static final Integer RABBITMQ_PORT=5672; public static final String RABBITMQ_USERNAME="guest"; public static final String RABBITMQ_PASSWORD="guest"; public static final String RABBITMQ_VIRTUAL_HOST="/"; public static final String QUEUE_NAME_ONE="pubsub-one"; public static final String QUEUE_NAME_TWO="pubsub-two"; public static final String EXCHANGE_NAME="pubsub"; //构建rabbitMQ链接对象 public static Connection getConnection() throws IOException, TimeoutException { //创建connection工厂 ConnectionFactory factory = new ConnectionFactory(); //设置RabbitMq 连接信息 factory.setHost(RABBITMQ_HOST); factory.setPort(RABBITMQ_PORT); factory.setUsername(RABBITMQ_USERNAME); factory.setPassword(RABBITMQ_PASSWORD); factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST); //返回链接对象 Connection connection = factory.newConnection(); return connection; } @Test public void publish() throws IOException, TimeoutException { //获取连接对象 Connection connection = PublishSubscribeTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); //构建队列 durable 队列是否持久化 exclusive 如果为true 只允许一个消费者消费 autoDelete 队列长时间没有使用 自动删除 channel.queueDeclare(QUEUE_NAME_ONE, false, false, false, null); channel.queueDeclare(QUEUE_NAME_TWO, false, false, false, null); //绑定交换机和队列 使用的是FANOUT类型交换机,绑定方式是直接绑定 channel.queueBind(QUEUE_NAME_ONE, EXCHANGE_NAME, ""); channel.queueBind(QUEUE_NAME_TWO, EXCHANGE_NAME, ""); //发布消息 使用rabbitMQ自带的默认交换机 routingkey 叫什么 就把消息路由到哪个队列中 for (int i = 0; i < 100; i++) { String message="Hello World"+i; channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes()); } System.out.println("消息发送成功"); System.in.read(); } @Test public void consumer1() throws Exception { //获取连接对象 Connection connection = PublishSubscribeTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME_ONE, false, false, false, null); //设置消息的流控 一次只消费一个 别一次性分发完后 让consumer自己慢慢消费 此设置是让consume消费完一条消息后再从channel中再获取下一条消息 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者一号获取到的消息:"+new String(body) ); channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME_ONE, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } @Test public void consumer2() throws Exception { //获取连接对象 Connection connection = PublishSubscribeTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME_TWO, false, false, false, null); //设置消息的流控 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者二号获取到的消息:"+new String(body) ); //deliveryTag 标识 channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME_TWO, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } }
Routing 手动创建Exchange(DIRECT)
@SpringBootTest class RoutingTests { public static final String RABBITMQ_HOST="192.168.1.137"; public static final Integer RABBITMQ_PORT=5672; public static final String RABBITMQ_USERNAME="guest"; public static final String RABBITMQ_PASSWORD="guest"; public static final String RABBITMQ_VIRTUAL_HOST="/"; public static final String QUEUE_NAME_ONE="routing-one"; public static final String QUEUE_NAME_TWO="routing-two"; public static final String EXCHANGE_NAME="routing"; //构建rabbitMQ链接对象 public static Connection getConnection() throws IOException, TimeoutException { //创建connection工厂 ConnectionFactory factory = new ConnectionFactory(); //设置RabbitMq 连接信息 factory.setHost(RABBITMQ_HOST); factory.setPort(RABBITMQ_PORT); factory.setUsername(RABBITMQ_USERNAME); factory.setPassword(RABBITMQ_PASSWORD); factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST); //返回链接对象 Connection connection = factory.newConnection(); return connection; } @Test public void publish() throws IOException, TimeoutException { //获取连接对象 Connection connection = RoutingTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建交换机 routing 类型交换机为DIRECT channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //构建队列 durable 队列是否持久化 exclusive 如果为true 只允许一个消费者消费 autoDelete 队列长时间没有使用 自动删除 channel.queueDeclare(QUEUE_NAME_ONE, false, false, false, null); channel.queueDeclare(QUEUE_NAME_TWO, false, false, false, null); //绑定交换机和队列 使用的是FANOUT类型交换机,绑定方式是直接绑定 需要指定routingkey channel.queueBind(QUEUE_NAME_ONE, EXCHANGE_NAME, "ORANGE"); channel.queueBind(QUEUE_NAME_TWO, EXCHANGE_NAME, "BLACK"); channel.queueBind(QUEUE_NAME_TWO, EXCHANGE_NAME, "GREEN"); //发布消息 使用rabbitMQ自带的默认交换机 routingkey 叫什么 就把消息路由到哪个队列中 需要指定routingkey 只有routingkey一致 才会将消息路由到指定的队列 for (int i = 0; i < 100; i++) { String message="Hello World"+i; channel.basicPublish(EXCHANGE_NAME, "ORANGE", null, message.getBytes()); channel.basicPublish(EXCHANGE_NAME, "BLACK", null, message.getBytes()); channel.basicPublish(EXCHANGE_NAME, "WHITE", null, message.getBytes());//没有队列和交换机绑定 white } System.out.println("消息发送成功"); System.in.read(); } @Test public void consumer1() throws Exception { //获取连接对象 Connection connection = RoutingTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME_ONE, false, false, false, null); //设置消息的流控 一次只消费一个 别一次性分发完后 让consumer自己慢慢消费 此设置是让consume消费完一条消息后再从channel中再获取下一条消息 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者一号获取到的消息:"+new String(body) ); channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME_ONE, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } @Test public void consumer2() throws Exception { //获取连接对象 Connection connection = RoutingTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME_TWO, false, false, false, null); //设置消息的流控 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者二号获取到的消息:"+new String(body) ); //deliveryTag 标识 channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME_TWO, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } }
Topics 手动创建Exchange(TOPIC) routing 规则 项目名+模块名+方法名
@SpringBootTest class TopicsTests { public static final String RABBITMQ_HOST="192.168.1.137"; public static final Integer RABBITMQ_PORT=5672; public static final String RABBITMQ_USERNAME="guest"; public static final String RABBITMQ_PASSWORD="guest"; public static final String RABBITMQ_VIRTUAL_HOST="/"; public static final String QUEUE_NAME_ONE="topic-one"; public static final String QUEUE_NAME_TWO="topic-two"; public static final String EXCHANGE_NAME="topic"; //构建rabbitMQ链接对象 public static Connection getConnection() throws IOException, TimeoutException { //创建connection工厂 ConnectionFactory factory = new ConnectionFactory(); //设置RabbitMq 连接信息 factory.setHost(RABBITMQ_HOST); factory.setPort(RABBITMQ_PORT); factory.setUsername(RABBITMQ_USERNAME); factory.setPassword(RABBITMQ_PASSWORD); factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST); //返回链接对象 Connection connection = factory.newConnection(); return connection; } @Test public void publish() throws IOException, TimeoutException { //获取连接对象 Connection connection = TopicsTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建交换机 routing 类型交换机为DIRECT channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); //构建队列 durable 队列是否持久化 exclusive 如果为true 只允许一个消费者消费 autoDelete 队列长时间没有使用 自动删除 channel.queueDeclare(QUEUE_NAME_ONE, false, false, false, null); channel.queueDeclare(QUEUE_NAME_TWO, false, false, false, null); //绑定交换机和队列 使用的是TOPIC类型交换机,需要以aaa.bbb.ccc方式编写toutingkey //其中*(相当于占位符),#(相当于通配符) channel.queueBind(QUEUE_NAME_ONE, EXCHANGE_NAME, "*.orange.*");//aaa.orange.bbb channel.queueBind(QUEUE_NAME_TWO, EXCHANGE_NAME, "*.*.rabbit");//aaa.orange.rabbit 以上两个都会路由 channel.queueBind(QUEUE_NAME_TWO, EXCHANGE_NAME, "lazy.#");//lazy.后面随便跟什么 都会路由到这里 //发布消息 使用rabbitMQ自带的默认交换机 routingkey 叫什么 就把消息路由到哪个队列中 需要指定routingkey 只有routingkey一致 才会将消息路由到指定的队列 for (int i = 0; i < 100; i++) { String message="Hello World"+i; channel.basicPublish(EXCHANGE_NAME, "big.orange.rabbit", null, message.getBytes());//路由两个 channel.basicPublish(EXCHANGE_NAME, "small.white.rabbit", null, message.getBytes());//路由一个 channel.basicPublish(EXCHANGE_NAME, "lazy.dog.dog.dog.dog.dog", null, message.getBytes());//匹配最后一个 } System.out.println("消息发送成功"); System.in.read(); } @Test public void consumer1() throws Exception { //获取连接对象 Connection connection = TopicsTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME_ONE, false, false, false, null); //设置消息的流控 一次只消费一个 别一次性分发完后 让consumer自己慢慢消费 此设置是让consume消费完一条消息后再从channel中再获取下一条消息 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者一号获取到的消息:"+new String(body) ); channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME_ONE, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } @Test public void consumer2() throws Exception { //获取连接对象 Connection connection = TopicsTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_NAME_TWO, false, false, false, null); //设置消息的流控 channel.basicQos(1); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者二号获取到的消息:"+new String(body) ); //deliveryTag 标识 channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume(QUEUE_NAME_TWO, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } }
RPC

@SpringBootTest class RPCTests { public static final String RABBITMQ_HOST="192.168.1.137"; public static final Integer RABBITMQ_PORT=5672; public static final String RABBITMQ_USERNAME="guest"; public static final String RABBITMQ_PASSWORD="guest"; public static final String RABBITMQ_VIRTUAL_HOST="/"; public static final String QUEUE_PUBLISHER="rpc-publisher"; public static final String QUEUE_CONSUMER="rpc-consumer"; //构建rabbitMQ链接对象 public static Connection getConnection() throws IOException, TimeoutException { //创建connection工厂 ConnectionFactory factory = new ConnectionFactory(); //设置RabbitMq 连接信息 factory.setHost(RABBITMQ_HOST); factory.setPort(RABBITMQ_PORT); factory.setUsername(RABBITMQ_USERNAME); factory.setPassword(RABBITMQ_PASSWORD); factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST); //返回链接对象 Connection connection = factory.newConnection(); return connection; } @Test public void publish() throws IOException, TimeoutException { //获取连接对象 Connection connection = RPCTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_PUBLISHER, false, false, false, null); channel.queueDeclare(QUEUE_CONSUMER, false, false, false, null); //发布消息 使用rabbitMQ自带的默认交换机 routingkey 叫什么 就把消息路由到哪个队列中 需要指定routingkey 只有routingkey一致 才会将消息路由到指定的队列 String uuid = UUID.randomUUID().toString(); for (int i = 0; i < 100; i++) { String message="Hello World RPC"+i; AMQP.BasicProperties pros=new AMQP.BasicProperties() .builder() .replyTo(QUEUE_CONSUMER) .correlationId(uuid) .build(); channel.basicPublish("", QUEUE_PUBLISHER, pros, message.getBytes());//路由两个 channel.basicConsume(QUEUE_CONSUMER,false, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String id = properties.getCorrelationId(); if(id!=null&&id.equals(uuid)){ System.out.println("接收到服务端响应"+new String(body,"utf-8")); } channel.basicAck(envelope.getDeliveryTag(), false); } }); } System.out.println("消息发送成功"); System.in.read(); } @Test public void consumer1() throws Exception { //获取连接对象 Connection connection = RPCTests.getConnection(); //构建Channel Channel channel = connection.createChannel(); //构建队列 channel.queueDeclare(QUEUE_PUBLISHER, false, false, false, null); channel.queueDeclare(QUEUE_CONSUMER, false, false, false, null); //监听消息 DefaultConsumer defaultConsumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者一号获取到的消息:"+new String(body) ); String resp="获取到了client信息"; String responseQueueName = properties.getReplyTo(); String uuid = properties.getCorrelationId(); AMQP.BasicProperties pros=new AMQP.BasicProperties() .builder() .correlationId(uuid) .build(); channel.basicPublish("", responseQueueName, pros, resp.getBytes()); channel.basicAck(envelope.getDeliveryTag(), false); } }; channel.basicConsume(QUEUE_PUBLISHER, false, defaultConsumer); System.out.println("开始监听队列"); System.in.read(); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类