SpringBoot+Mysql+Redis+RabbitMQ+Jmeter模拟实现高并发秒杀
文章前言
众所周知,当遇到比较多数据不一致的问题时,大多数都是因为并发请求时,没及时处理的原因,提一个电商平台比较经常出现得高并发场景限时秒杀活动,他们是怎么来防止超卖呢?如何实现高并发秒杀呢?。
本文模拟了高并发秒杀,并且防止了超卖,也模拟了纯数据库秒杀超卖得场景,本次模拟demo得框架技术为:SpringBoot+Mysql+Redis+RabbitMQ+tkmybatis
数据库表结构:
一个为库存表,一个为订单表,本人使用得是mysql8.0。
完整得项目工具展示
Jmeter :
redisManager :
RabbitMQ :
编写代码
1.首先新建Springboot项目
2.可以先不勾选需要得jar包,项目初始化好之后,使用maven导入项目需要得jar包
pom.xml :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional> true </optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional> true </optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version> 3.8 . 1 </version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version> 0.7 . 0 </version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version> 2.1 . 0 </version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version> 2.0 . 3 -beta1</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version> 4.0 . 0 </version> </dependency> </dependencies> |
3.配置application.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | spring.devtools.restart.enabled= false ##配置数据库连接 spring.datasource.username=root spring.datasource.password=root server.port= 8443 spring.datasource.url=jdbc:mysql: //localhost:3306/ktoa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true spring.datasource.driver- class -name=com.mysql.cj.jdbc.Driver ##配置rabbitmq连接 spring.rabbitmq.host=localhost spring.rabbitmq.port= 5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest ##配置连接redis --都记得打开服务 spring.redis.host=localhost spring.redis.port= 6379 spring.redis.jedis.pool.max-active= 1024 spring.redis.jedis.pool.max-wait=-1s spring.redis.jedis.pool.max-idle= 200 spring.redis.password= 123456 |
这时可以启动一下springboot项目是否能够正常启动,如没问题可以继续往下编写!!
4.新建pojo包,添加实体类
Order.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import lombok.Data; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Data @Table (name = "t_order" ) public class Order implements Serializable { private static final long serialVersionUID = -8867272732777764701L; @Id @Column (name = "id" ) @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; @Column (name = "order_name" ) private String order_name; @Column (name = "order_user" ) private String order_user; } |
Stock.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import lombok.Data; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Table (name = "stock" ) @Data public class Stock implements Serializable { private static final long serialVersionUID = 2451194410162873075L; @Id @Column (name = "id" ) @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; @Column (name = "name" ) private String name; @Column (name = "stock" ) private Long stock; } |
因为本次数据库操作方面使用了tkmybatis框架,所以实体类我们需要用到JPA的注解,来实现映射关系!!
5.配置tkmybatis得接口
新建名为base得包,在base下面新建service得接口
GenericMapper.interface:
1 2 3 4 | import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; public interface GenericMapper<T> extends Mapper<T>, MySqlMapper<T> { } |
关于这个接口得作用你需要了解太多,你只要知道我们得mapper层需要通过继承它来实现数据库操作,如果你接触过jpa或者mybatis-plus,tkmybatis方式跟它们相似。
6.新建mapper层
新建名为mapper得包,在这个包下面新建
OrderMapper.interface:
1 2 3 4 5 6 7 | import com.spbtrediskill.secondskill.base.service.GenericMapper; import com.spbtrediskill.secondskill.pojo.Order; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OrderMapper extends GenericMapper<Order> { void insertOrder(Order order); } |
StockMapper.interface:
1 2 3 4 5 6 | import com.spbtrediskill.secondskill.base.service.GenericMapper; import com.spbtrediskill.secondskill.pojo.Stock; import org.apache.ibatis.annotations.Mapper; @Mapper public interface StockMapper extends GenericMapper<Stock> { } |
7.编写RabbitMQ和redis得配置类
新建config包,新建redis和RabbitMQ得类
MyRabbitMQConfig.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.ExchangeBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; @Configuration public class MyRabbitMQConfig { //库存交换机 public static final String STORY_EXCHANGE = "STORY_EXCHANGE" ; //订单交换机 public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE" ; //库存队列 public static final String STORY_QUEUE = "STORY_QUEUE" ; //订单队列 public static final String ORDER_QUEUE = "ORDER_QUEUE" ; //库存路由键 public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY" ; //订单路由键 public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY" ; @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } //创建库存交换机 @Bean public Exchange getStoryExchange() { return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable( true ).build(); } //创建库存队列 @Bean public Queue getStoryQueue() { return new Queue(STORY_QUEUE); } //库存交换机和库存队列绑定 @Bean public Binding bindStory() { return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs(); } //创建订单队列 @Bean public Queue getOrderQueue() { return new Queue(ORDER_QUEUE); } //创建订单交换机 @Bean public Exchange getOrderExchange() { return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable( true ).build(); } //订单队列与订单交换机进行绑定 @Bean public Binding bindOrder() { return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs(); } } |
RedisConfig .java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { // 配置redis得配置详解 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer( new StringRedisSerializer()); template.setValueSerializer( new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer( new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer( new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } } |
8.编写service层
新建service包以及impl包,这里只提供实现类,接口可以自行编写
OrderServiceImpl .java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import com.spbtrediskill.secondskill.mapper.OrderMapper; import com.spbtrediskill.secondskill.pojo.Order; import com.spbtrediskill.secondskill.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void createOrder(Order order) { orderMapper.insert(order); } } |
StockServiceImpl.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import com.spbtrediskill.secondskill.mapper.StockMapper; import com.spbtrediskill.secondskill.pojo.Stock; import com.spbtrediskill.secondskill.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import tk.mybatis.mapper.entity.Example; import java.util.List; @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; // 秒杀商品后减少库存 @Override public void decrByStock(String stockName) { Example example = new Example(Stock. class ); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo( "name" , stockName); List<Stock> stocks = stockMapper.selectByExample(example); if (!CollectionUtils.isEmpty(stocks)) { Stock stock = stocks.get( 0 ); stock.setStock(stock.getStock() - 1 ); stockMapper.updateByPrimaryKey(stock); } } // 秒杀商品前判断是否有库存 @Override public Integer selectByExample(String stockName) { Example example = new Example(Stock. class ); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo( "name" , stockName); List<Stock> stocks = stockMapper.selectByExample(example); if (!CollectionUtils.isEmpty(stocks)) { return stocks.get( 0 ).getStock().intValue(); } return 0 ; } } |
9.配置rabbitmq得实现方式以及redis得实现方式
在 service包下面新建 MQOrderService.java
这个类属于订单得消费队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import com.spbtrediskill.secondskill.config.MyRabbitMQConfig; import com.spbtrediskill.secondskill.pojo.Order; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class MQOrderService { @Autowired private OrderService orderService; /** * 监听订单消息队列,并消费 * * @param order */ @RabbitListener (queues = MyRabbitMQConfig.ORDER_QUEUE) public void createOrder(Order order) { log.info( "收到订单消息,订单用户为:{},商品名称为:{}" , order.getOrder_user(), order.getOrder_name()); /** * 调用数据库orderService创建订单信息 */ orderService.createOrder(order); } } |
MQStockService.java:
这个属于库存得消费队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import com.spbtrediskill.secondskill.config.MyRabbitMQConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class MQStockService { @Autowired private StockService stockService; /** * 监听库存消息队列,并消费 * @param stockName */ @RabbitListener (queues = MyRabbitMQConfig.STORY_QUEUE) public void decrByStock(String stockName) { log.info( "库存消息队列收到的消息商品信息是:{}" , stockName); /** * 调用数据库service给数据库对应商品库存减一 */ stockService.decrByStock(stockName); } } |
RedisService.java:
这个配置类,主要用来实现对redis得key和value初始化以及对value得操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.Date; import java.util.concurrent.TimeUnit; @Service public class RedisService { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 设置String键值对 * @param key * @param value * @param millis */ public void put(String key, Object value, long millis) { redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES); } public void putForHash(String objectKey, String hkey, String value) { redisTemplate.opsForHash().put(objectKey, hkey, value); } public <T> T get(String key, Class<T> type) { return (T) redisTemplate.boundValueOps(key).get(); } public void remove(String key) { redisTemplate.delete(key); } public boolean expire(String key, long millis) { return redisTemplate.expire(key, millis, TimeUnit.MILLISECONDS); } public boolean persist(String key) { return redisTemplate.hasKey(key); } public String getString(String key) { return (String) redisTemplate.opsForValue().get(key); } public Integer getInteger(String key) { return (Integer) redisTemplate.opsForValue().get(key); } public Long getLong(String key) { return (Long) redisTemplate.opsForValue().get(key); } public Date getDate(String key) { return (Date) redisTemplate.opsForValue().get(key); } /** * 对指定key的键值减一 * @param key * @return */ public Long decrBy(String key) { return redisTemplate.opsForValue().decrement(key); } } |
下面为service包得完整目录:
10.编写controller层
在新建得controller包下面新建类 SecController.java
该controller提供了二个方法,一个为redis+rabbitmq实现高并发秒杀,第二个则用纯数据库模拟秒杀,出现超卖现象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | import com.spbtrediskill.secondskill.config.MyRabbitMQConfig; import com.spbtrediskill.secondskill.pojo.Order; import com.spbtrediskill.secondskill.service.OrderService; import com.spbtrediskill.secondskill.service.RedisService; import com.spbtrediskill.secondskill.service.StockService; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @Slf4j public class SecController { @Autowired private RabbitTemplate rabbitTemplate; @Autowired private RedisService redisService; @Autowired private OrderService orderService; @Autowired private StockService stockService; /** * 使用redis+消息队列进行秒杀实现 * * @param username * @param stockName * @return */ @PostMapping ( value = "/sec" ,produces = "application/json;charset=utf-8" ) @ResponseBody public String sec( @RequestParam (value = "username" ) String username, @RequestParam (value = "stockName" ) String stockName) { log.info( "参加秒杀的用户是:{},秒杀的商品是:{}" , username, stockName); String message = null ; //调用redis给相应商品库存量减一 Long decrByResult = redisService.decrBy(stockName); if (decrByResult >= 0 ) { /** * 说明该商品的库存量有剩余,可以进行下订单操作 */ log.info( "用户:{}秒杀该商品:{}库存有余,可以进行下订单操作" , username, stockName); //发消息给库存消息队列,将库存数据减一 rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, stockName); //发消息给订单消息队列,创建订单 Order order = new Order(); order.setOrder_name(stockName); order.setOrder_user(username); rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, order); message = "用户" + username + "秒杀" + stockName + "成功" ; } else { /** * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户 */ log.info( "用户:{}秒杀时商品的库存量没有剩余,秒杀结束" , username); message = "用户:" + username + "商品的库存量没有剩余,秒杀结束" ; } return message; } } |
纯数据库秒杀方式得方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /** * 实现纯数据库操作实现秒杀操作 * @param username * @param stockName * @return */ @RequestMapping ( "/secDataBase" ) @ResponseBody public String secDataBase( @RequestParam (value = "username" ) String username, @RequestParam (value = "stockName" ) String stockName) { log.info( "参加秒杀的用户是:{},秒杀的商品是:{}" , username, stockName); String message = null ; //查找该商品库存 Integer stockCount = stockService.selectByExample(stockName); log.info( "用户:{}参加秒杀,当前商品库存量是:{}" , username, stockCount); if (stockCount > 0 ) { /** * 还有库存,可以进行继续秒杀,库存减一,下订单 */ //1、库存减一 stockService.decrByStock(stockName); //2、下订单 Order order = new Order(); order.setOrder_user(username); order.setOrder_name(stockName); orderService.createOrder(order); log.info( "用户:{}.参加秒杀结果是:成功" , username); message = username + "参加秒杀结果是:成功" ; } else { log.info( "用户:{}.参加秒杀结果是:秒杀已经结束" , username); message = username + "参加秒杀活动结果是:秒杀已经结束" ; } return message; } |
11.编写springboot启动类
最后一步我们需要在springboot得启动类中进行对redis得初始化,简而言之就是调用我们上面写得方法,新建一个redis缓存,模拟商品信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import com.spbtrediskill.secondskill.service.RedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import tk.mybatis.spring.annotation.MapperScan; @SpringBootApplication @MapperScan ( "com.spbtrediskill.secondskill.mapper" ) public class SecondskillApplication implements ApplicationRunner{ public static void main(String[] args) { SpringApplication.run(SecondskillApplication. class , args); } @Autowired private RedisService redisService; /** * redis初始化商品的库存量和信息 * @param args * @throws Exception */ @Override public void run(ApplicationArguments args) throws Exception { redisService.put( "watch" , 10 , 20 ); } } |
项目得整个目录:
至此我们得项目代码就编写完成了,记得仔细检查是否有遗漏,下面准备进入最重要得测试环节!!
测试前提
上面代码编写完整之后我们可以启动springboot,启动之前记得打开redis和rabbitmq得服务,检查是否出错:
启动成功之后打开Redis Desktop Manager工具,查看是否新建了一个redis :watch、
ok,如果好了,现在打开我们得JMeter工具,可能有些人对这个工具很陌生,下面我教大家如何使用JMeter,大佬忽略!!
首先选择中文
完成中文之后,我们在测试计划右键,添加一个线程组
给这个线程组得数量为40,这个线程组得作用就是模拟40个用户发送请求,去秒杀.
然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求得组件了
这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数
点击上方得白色小书本,选择random,1-99得随机数:
然后我们把这个函数字符串复制到http得参数上面去:
最后我们在测试计划建一个结果树,查看我们发送请求返回得消息数据:
这些完成之后我们就可以开始发送请求了运行run
测试结果–redis+rabbitmq
运行之后查看我们得控制台:
可以看到日志已经打印到控制台了,用户名为我们生成得随机数。
再来看下数据库订单表order:
图中有10条秒杀到商品得用户信息和商品名,我再帮大家理一理,我们初始化得时候给watch库存得数量为10,而我们使用JMeter模拟了40个人发请求,所以这10条数据,也就是40个用户中抢到商品得10个人,也就是线程,谁抢到就是谁得。
再来查看下我们得结果树:
结果树上面有40条请求信息,通过其中我们可以看的每条请求得详细数据以及返回得值。
现在我们再打开redismanager,其中我们初始化为10,现在是-30,可以知道有40个线程去获取了它,现在为-30,每次前测试记得,手动清空缓存!!一定要记得
纯数据库方式秒杀结果
上面我们实现了redis+rabbitmq得秒杀,现在我们看看纯数据库方式得秒杀,看看有什么区别:
1.首先网stock库存表新增一条数据,类似于redis得初始化
2.在jmeter中修改原来得http请求信息,其中小米对应数据库得商品名
清空一下结果树,我们开始运行
3.run
控制台:
重要得是查看数据库得信息:
库存已经清空,再看order表
这样我们可以看到,明明只有10个库存得商品,抢到得人却不止10个,这样明细超卖了,请求树也可以看的超卖信息
总结
从这二个方式实现得秒杀就可以知道二者得区别,以及大概得了解这个过程是怎么实现得,写这篇文章得主要初衷是方便那些刚接触这方面得小白,没有人刚来什么都会。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具