[八股背诵]
http 1.0
无状态,无连接,
无法复用连接,多次的建立断开连接导致性能不好,
队头阻塞,下一个请求必须等待上一个请求返回,
http 1.1
长连接
多次请求可以使用同一个TCP连接
请求管道化
下一个请求不必等待上一个请求返回,但是在服务端必须按照顺序返回结果,这样浏览器才能识别这次返回的是哪个请求要的结果,导致队头阻塞的问题从浏览器转移到服务器
但是实际上管道化很鸡肋,一般情况下浏览器会选择开启多个TCP连接并行响应
HTTP 2.0
● 二进制分帧
不再使用json文本格式传输数据,而是使用二进制的序列化方式,所以GRPC底层就是用的http2.0
● 多路复用
同一个域名下的所有请求在同一个连接内完成,
请求可以乱序发送,响应也可以乱序返回,
http报文被切割成更小的帧,帧之间可以乱序发送,进一步提高了信息传送的效率
● 服务器推送
浏览器请求html的时候服务器会主动推送js和css
● 头部压缩
2.0以前的版本每个http报文会携带请求头,http2.0会把其中一些信息缓存起来,不需要每次都传送这些重复数据
HTTP 和 RPC的区别
● 服务发现
一般情况下http使用dns服务从网址得到对应的ip地址,以及默认的端口号80 443
但是rpc使用了专门的服务注册组建,我们先去这个组件里查询,对应的服务在那台机器的某个端口上,
在这个方面两者有区别,但是区别不大
● 复用连接
http1.1 使用的是连接复用,也就是多个请求在同一个链接中发送,keep-alive
rpc也是复用连接但是另外也会构造一个连接池,把用完的tcp连接再放回连接池,如果下次需要用了直接把之前的连接拿出来
● 序列化
普通的http1.0 http1.1使用的是json序列化,也就是要先把对象转换成json字符串,然后再转成二进制序列,反序列化的时候要先把二进制解析成字符串,再从字符串里读取键值对,再转换成对象,这样的序列化很费时间,另外,json里还存储了很多无用的字符,比如括号和键,增加了传输的负担,另外http报文还需要许多的请求头字段,如果我们约定好解析规则和请求头,就不需要传输这么多的字段,还有http报文头很多字段是为了浏览器和服务器之间的通信设计的,比如什么useragent,重定向了,但是如果我们是在微服务和微服务之间通信,就不需要这些字段了,综合以上几点,rpc的序列化既节省了时间,又节省了字节,比http更为有利,但是呢,http2.0,也用了二进制分帧,不再使用json序列化协议,所以http2.0的性能也得到了很大的提升,grpc的底层就直接使用了http2.0
JWT
● 头部
签名部分用的什么加密算法,实际上是一个json字符串,需要进行base64编码得到一个很长的字符串
● 载荷,实际上也是一个json字符串,也需要进行base64编码
○ 标准声明
■ 谁签发的jwt
■ Jwt发送给谁
■ jwt有效时间
○ 公共声明
■ 你想要传输的有效信息,例如用户的id用户的名字
○ 私有声明
● 签名
○ 把上面两个用base64编码的字符串用.连接起来,再用头部中指名的签名算法加盐,一定要注意加盐,实际上是把服务器端生成的一个私钥加进去了,进行加密,这样,只有知道这个私钥的人才能正确的进行加密和解密
注意:jwt的有效信息实际只是被base64编码了而已,并没有进行加密隐藏,所以不要在这里面加一些敏感数据
另外,头部中的签名算法也没有被隐藏,但是入侵者仅仅知道这个加密算法是没有用的,还需要知道私钥才行
使用jwt不需要查库,以前使用cookie存储session中的key的时候,必须要在拿到cookie之后去库里验证这个cookie的真假,但是使用密钥加密本身就带有了验证正确性的功能,因为只有拥有密钥的真正的服务器才能完成加密解密
http 和 https
http和https的区别就在于https在TCP连接之后还会进行一次TLS握手,保证加密传输
另外端口号不同http 80 https 443
其次https还涉及到数字证书技术
下面介绍TLS的握手过程
● 首先浏览器发送client hello
○ TLS版本号
○ 加密算法比如RSA算法
○ 第一个随机数client random
● 然后服务器回复server hello
○ 确认TLS版本号
○ 确认加密算法
○ 第二个随机数server random
○ 网站的数字证书
● 然后浏览器回复
○ 浏览器会验证证书的有效性,如果证书是有效的,取到证书中的网站公钥
○ 利用网站公钥加密传输第三个随机数字
○ 从此以后,浏览器有了三个随机数也就有了加密传输的密钥
● 然后服务器回复
○ 从此以后浏览和网站之间使用三个随机数产生的密钥进行对称加密
常见网络状态码
1请求已被接受但需要后续处理
● 100客户端应继续发送请求
● 101切换协议
2请求已被接受
200成功执行,响应被返回
201成功执行,请求已被响应并且创建了新资源
202成功执行,服务器接受请求,但是尚未处理
204成功执行,不包含任何消息体,但是不应该引起用户视图的变化
205成功执行,不包含任何消息体,要求引起用户视图的变化
3
301永久重定向,缓存服务器的重定向信息,网站已经永久迁移到另一个网站,此后对这个网站的访问都应该被重定向到返回的这几个网址
302临时重定向,不缓存返回的网址,只是临时跳转到返回的网址,因为下次访问这个网址可能就是通的了
4
400请求包含语法错误
401需要进行身份验证
403服务器已经理解你的请求,但是解决拒绝执行
404你访问的资源在服务器上未找到
5
500服务器执行出错了
502网关收到了无效的响应
504网关长时间没有收到响应,一般就是背墙了
100浏览器要继续发送消息
101切换协议
200成功响应
201成功响应,建立资源
202接受请求,但暂未执行
204成功返回,无消息体,不引起浏览器视图变化
205成功返回,无消息体,引起浏览器视图变化
301永久重定向,缓存返回的网址
302临时重定向,不缓存返回的网址
400请求中含有语法错误
401要求进行用户身份验证
403理解请求但拒绝执行
404你要访问的资源未找到
500服务器执行出错了
502网关收到了无效响应
504网关长时间未收到响应
HTTPS如何保证数据完整性
加密
数字证书
数据摘要
类加载机制
● 加载
○ 通过类的全限定名来加载这个类的二进制字节流
○ 在内存中生成这个类的模版
○ 生成类的class对象
● 验证
○ 验证class文件的正确性
● 准备
○ 为类的静态变量分配空间,初始化为零值
● 解析
○ 把常量池中的符号引用转换成直接引用,也就是定位到这些目标的位置
● 初始化
○ 执行类的clinit函数,包括静态变量的赋值语句和static块
● 使用
● 卸载
什么时候触发类的初始化,也就是类的主动使用
new关键字
调用类的静态方法
使用类的静态变量
反射调用
获取class对象
获取字段
获取方法
执行方法
初始化子类的时候发现父类没有初始化,会先初始化父类
虚拟机启动的时候会先初始化主类,也就是main函数所在的类
GCROOT对象
● 线程栈帧中引用的对象
● 类的静态变量引用的对象
● 常量池引用的对象
● 被同步锁锁住的对象
索引失效
未遵循最左匹配原则
在索引列上使用了函数或是计算
or的错误使用,比如有两个列,一个建了索引,一个没有建索引,这条语句是不会走索引的,因为总是要全表扫描
使用了like %xxx
in exists 后面跟了大量数据
隐式类型转换,如果索引列本来是字符串,却用数字来匹配就会不索引
order by 如果数据直接在索引中,遍历索引就可以得到结果,但是如果要查的数据不在索引里需要回表,这个时间消耗是很大的,索引数据会会选择直接全表扫描
redolog的刷盘时机
● mysql正常关闭的时候
● redolog buffer写入超过一半的时候
● 后台线程每隔一秒写入磁盘一次
○ 0
○ 2
● 事务提交的时候
○ 0每次提交事务,redolog留在buffer中
○ 1每次提交事务,redolog写入磁盘
○ 2每次提交事务,redolog写入操作系统的文件缓存页面
binlog的刷盘时机
● 0每次提交事务只写到操作系统的文件缓存,交给操作系统合适刷盘
● 1每次提交事务都执行fsync刷盘
● N,N次提交事务,有一次fsync
为什么需要redolog
● 提供了崩溃恢复的能力,保证了事务的持久性
● 把磁盘操作从随机写变成顺序写
为什么需要undolog
● 保证事务的原子性
● 配合readview实现了mvvc机制
为什么有了binlog还要redolog
一开始只有binlog,但是binlog不知道那些操作是已经对数据页进行了修改,哪些
redolog可以知道哪些操作还没有对bufferpool中的数据进行写入
redolog会把已经刷入磁盘的脏页数据的修改记录给删掉,这样留在redolog中的都是还没有被刷入磁盘的脏页的修改记录
redolog和binlog的区别
● 文件格式不同
● 目的不同
● 是否全量
主从复制
● binlog dump
● IO relay log
● SQL
主从复制模型
● 同步
● 异步
● 半同步
java异常
● throwable
○ error
■ out of memory
■ stackoverflowerror
○ exception
■ runtimeexception
● indexoutofbound
● nullpointer
■ 其他异常
● IO
● sql
● classnot found (比如想通过类的全限定名获得class对象)
获取class对象的方式
class.forname()
类名.class
对象.getclass
object类的方法
● hashcode
● equals
● wait
● wait()
● wait()
● notify
● notifyall
● tostring
● clone
● getclass
慢查询
● 首先开启慢查询日志
○ slow_query_log=1
○ 设置文件地址
○ 设置时间,多长时间是慢查询
○ 有两类语句不会被记录,一类是数据管理语言,一类是没有走索引的语句
● 查看慢查询日志
○ 是不是所获取时间太长了
○ 是不是返回来的数据太多了
● 分析慢查询日志
○ mysqldumpslow
● explain
explain
● selectid id越大优先级越高
● select_type 子查询 主查询 union
● table 对哪张表查询
● type
○ const就一行
○ ref使用索引,结果可能有多行
○ range对索引列范围查询
○ index全索引扫描
○ all全表扫描
● possiable key
● key
● key len
● rows 估计返回行数
● extra
○ using index 索引覆盖
○ using index condition 索引下推
○ using temp 创建临时表
○ using filesort 文件排序
延迟消息队列
死信队列
要想使用延迟消息队列必须要有下面几个组件
● 带有过期时间的队列,当消息在这种队列里存在超过一定时间,就会被重新投递到死信交换机
● 死信交换机,根据消息上带有的死信绑定消息,投递到相对应死信队列
● 死信队列,死信交换机把消息投递到这里,消费者从这里取出消息消费
另外,每条消息上要绑定两个路由信息,一个是路由到带有过期时间队列,一个是路由到死信队列
缓存穿透
请求一个不存在的数据,先去查缓存,发现缓存没有,再去查数据库,发现也没有,这样就做了两次无用的查询
设置布隆过滤器
如果他告诉我们这个数据不存在那就一定不存在,如果他说存在,那也不一定存在
原理是,使用多个哈希函数,把一个key值使用多个哈希函数进行运算,把相应的位置设置为1,使用多个哈希函数是为了避免哈希冲突,
哈希冲突会导致,例如A和B使用同一个哈希函数产生了哈希冲突,先把A放进缓存,然后我们想要获取B的时候发现相应位置上的标志位已经被设置了,那我们以为这个数据是存在的,但实际上这个位置是因为哈希冲突被另一个数据标志的,增加哈希函数可以降低哈希冲突的概率
缓存击穿
缓存删除了一个热点数据,导致对这个热点数据的大量的请求直接发到了数据库中,
热点数据永不过期
设置互斥锁,只有一个线程能去数据库获取数据
这种情况下,线程的逻辑应该是这样的,首先去查缓存,如果缓存里没有,尝试去获取一个分布式锁(这里实际上就是在redis中set一个键值对set ex nx),谁先设置成功了谁就获得了这个锁,然后是获得🔒的线程去查询数据库,如果查到了把数据写回缓存,如果发现没有,也要把数据写回缓存,只是要把值设置成Null,让其他线程知道数据库里没有这个数据,其他被互斥锁阻塞的线程的操作应该是这样的,尝试获取锁,但是这个操作是有过期时间的,超过一定时间还没有获取到锁,就再次去查询缓存,看看缓存里是不是有数据了,有数据也分两种情况,一种是真的数据,一种是值为null的数据,不管哪种,线程都会结束循环,但是如果查询缓存的时候发现还是没有这个数据,那就继续尝试获取锁
缓存雪崩
随机设置过期时间,避免同时大量热点数据过期
设置互斥锁,只允许一个线程去数据库拿数据
包装类的作用
● 允许null值
● 提供了泛型支持
● 提供了集合框架的支持
● 包装类提供了很多方法,比如可以在把字符串转换成包装类
Redis中的底层数据结构
string emdstr raw
list quicklist
map hashmap ziplist
set hashmap inset
zst skiplist ziplist
Redis hashmap扩容
扩容条件:
如果正在bgsave bgrewriteaof,负载因子到达5
如果没有正在持久化,负载因子到达1
缩容条件:
负载因子达道十分之一
扩容后容量:
原数组的两倍
缩绒后容量:
大于原数组的Used的第一个2的次方
渐进式rehash
不是一口气把h0的元素重新挂载到h1,而是在后面对h的操作中每次转移一个槽位
如果后面没有操作那就在是在定时任务中rehash
为什么emdstr和raw的分界线是44
redis object对象头结构是16
sds的除了content结构是3
content中有一个字节的结束符号
主要原因是整个超过64的时候就不会进行一次内存申请而是转成raw进行两次内存申请
SDS
一个字节的 capacity
一个字节的 length
一个字节的 flag
content数组
TCP和UDP的区别
● TCP需要先建立连接,UDP不需要建立链接
● TCP有确认和重传机制来保证可靠性,UDP没有
● TCP是面向字节流的,也就是说,TCP会把整个消息切割成,接收方再组装起来,UDP是面向报文的,消息是完整的
● TCP适合对可靠性要求高的,比如文件传输,UDP适合对实时性要求高的,比如视频通话这种
● TCP首部有二十个字节,UDP首部只有8字节
Java线程安全的集合类型
● vector
● hashtable
● concurrenthashmap
进程之间的通信方式
管道
信号量
消息队列
共享内存
socket
线程之间的通信方式
volatile
join
wait notify
await singal
inheritabelthreadlocal
syncronized 和 lock
等待可中断
构建公平锁
创建多个等待队列
零拷贝原理
read write
四次切换,两次DMA,两次CPU
mmap
四次切换,两次DMA,一次CPU
sendfile
两次切换,两次DMA,一次CPU
sendfile 新版本
两次切换,两次DMA,0次CPU
事务传播
propagation.required 如果a已经存在一个事务,那b就在这个事务中运行;
如果a不存在一个事务,那b就开启一个事务
propagation.supports 如果a已经存在一个事务,那b就在这个事务中运行;
如果a不存在一个事务,那b就在非事务状态下运行
propagation.mandatory 如果a已经存在一个事务,那b就在这个事务中运行;
如果a不存在一个事务,那b就抛出一个异常
propagation.requires_new 不论外部事务如何,新开一个事务运行;
如果a已经存在一个事务,那就挂起a的事务;
propagation.not_supported 不论外部事务如何,以非事务状态运行;
如果a已经存在一个事务,那就挂起a的事务
propagation.never 不论外部事务如何,以非事务状态运行;
如果a已经存在一个事务,那就抛出异常
propagation.nested 如果a已经存在一个事务,那b新开一个嵌套事务;
如果a不存在一个事务,那b新开一个事务
执行一条sql语句的过程
● 建立链接
○ TCP三次握手
○ 账号密码输入正确
○ 返回连接权限
● 查询缓存
○ 于8.0版本删除
○ 如果查询表a产生了一条缓存c,那么在更新a的时候,缓存c就会被删除
● 解析器
○ 词法分析:识别出关键字和字段等等信息
○ 语法分析:判断这是不是一条合法的语句
● 执行器
○ 预处理
■ 把*改成真正的字段
■ 字段和表是不是真的存在
○ 优化
■ 决定使用哪个索引还是全表扫描
■ 使用索引覆盖,索引下推等等策略
○ 执行
■ 根据优化器决定的执行策略,查索引或者全表扫描
事务传播策略
propagation.required 如果a已经开启了事务,那就在a事务中继续运行;如果a没有开启事务,那b就开启一个事务
propagation.supports 如果a已经开启了事务,那就在a事务中继续运行;如果a没有开启事务,那b就以非事务状态运行
propagation.mandatory 如果a已经开启了事务,那就在a事务中继续运行; 如果a没有开启事务,那b就抛出异常
propagation.requires_new 如果a已经开启了事务,那就挂起这个事务,开启一个新的事务,否则直接开启一个新的事务
propagation.not_supported 如果a已经开启了事务,那就挂起这个事务,以非事务状态运行
propagation.never 如果a已经开启了事务,那就抛出异常,以非事务方式运行
propagation.nested 如果a已经开启了事务,b开启一个嵌套事务,如果a没有开启事务,那b就开启一个事务,如果b回滚不会影响a,如果a回滚会影响b
事务传播策略
支持当前事务
propagation.supports 如果a已经开启了事务,b就使用a的事务;如果a没有开启事务,就按照非事务状态运行
propagation.required 如果a已经开启了事务,b就使用a的事务;如果a没有开启事务,b就开启一个事务
propagation.mandatory 如果a已经开启了事务,b就使用a的事务;如果a没有开启事务,b就抛出异常
不支持当前事务
propagation.requries_new 如果a已经开启了事务,b就挂起当前事务,开启一个新事物;如果a没有开启事务,b就创建一个新事务
propagation.not_spported 如果a已经开启了事务,b就挂起当前事务,已非事务状态运行;如果a没有开启事务,b就以非事务状态运行
propagation.never 如果a已经开启了事务,b就抛出异常;如果a没有开启事务,以非事务状态运行;如果a没有开启事务,b就已非事务状态运行
propagation.nested 如果a没有开启事务,b就开启事务,如果a已经开启了事务,b开启一个嵌套事务
CAP/BASE
CAP原理
C:一致性
A:可用性
P:分区容忍性
这个原理出现的前提是要先有网络分区,在网络分区出现的情况下,要么保证一致性,不对外服务,要么保证可用性,牺牲一部分持久性
在大多数情况下,我们都会选择保证可用性,牺牲一部分一致性,但是牺牲一部分一致性不代表不追求一致性,我们在这种情况下追求的是最终一致性,这就是BASE原理在CAP基础上延伸的地方
BASE原理描述的是
BA基本可用,牺牲一部分功能,或者让响应变慢
S软状态,出现不一致
E最终一致性,经过一段时间之后实现一致性
Raft协议
基本概念
leader
candidation
follower
事务传播
propagation.required 支持当前事务,如果当前无事务,就新开一个事务|如果
propagation.supports 支持当前事务,如果当前无事务,就以非事务状态运行
propagation.mandatory 支持当前事务,如果当前无事务,就抛出异常
propagation.requires_new 不支持当前事务,如果当前存在事务,就挂起当前事务,开一个新事物|
propagation.not_supported 不支持当前事务,如果当前存在事务,就挂起当前事务,以非事务状态运行
propagation.never 始终以非事务状态运行,如果当前存在事务,抛出异常
propagation.nested 如果当前无事务,就开启一个新事务;如果当前有事务,就开启一个嵌套事务|如果a回滚b也会回滚,如果b回滚,a不会回滚
锁的分类
● 乐观锁
● 悲观锁
● 公平锁
● 非公平锁
● 排他锁
● 共享锁
● 意向锁
● 可重入锁
● 分段锁
● 无锁
synchronized锁对象刚刚创建的时候,没有线程来获取这个锁对象,此时是无锁状态
● 偏向锁
当第一个线程获取这个锁对象的时
● 轻量级锁
● 重量级锁
索引选型
哈希表:不能顺序查找,也不能范围查找
二叉查找树:不够平衡,查找高度会很高,相应的IO次数比较多
平衡二叉树:IO次数比较多,需要频繁的旋转保证平衡
红黑树:不够平衡,树的高度比较高,需要多次io
B-树:树的高度比较高,不合适范围查找
B+树:树的高度被压缩,叶子节点在同一层通过链表相连,适合范围查找
跳表:树的高度比较高,但是胜在实现极其简单
MySQL为什么选择b+树
● Mysql是存储在磁盘上的,所以最好不要太多次的IO操作,B+树通过在非叶子节点只存索引,实现了一个节点里可以存储很多的分支,大大压缩了树的高度,降低了IO次数
● 跳表不太适合磁盘IO因为跳表没有做树高的压缩,一个节点上只有一个数据索引,所以从上到下要进行多次磁盘IO
redis为什么选择跳表
● 实现极其简单
● 在基于内存的redis中,没有磁盘io的烦恼
● 跳表在频繁执行
● 范围查找的时候性能表现也很好
● 跳表还可以更好的节省内存,通过压缩跳表晋升的几率,可以做到更少的内存消耗,跳表多出来的内存实际上是多个指针的消耗,
事务传播机制
propagation.required 支持当前事务 如果当前无事务,新建一个事务
propagation.supports 支持当前事务 如果当前无事务,以非事务状态运行
propagation.mandatory 支持当前事务 如果当前无事务,抛出异常
propagation.requires_new 不支持当前事务,如果当前有事务,挂起当前事务,开启一个新的事务,如果当前无事务,开启一个新的事务
propagation.not_supported 不支持当前事务,如果当前有事务,挂起当前事务,以非事务状态运行
propagation.never 不支持事务,如果有有事务,抛出异常
propagation.nested 支持嵌套事务,如果当前无事务,开启一个事务,如果当前有事务,开启一个嵌套事务,a回滚b回滚,b回滚a
JVM垃圾回收器
Serial
- 单线程垃圾回收器
- 面向全堆
- 在新生代上标记 + 复制
- 在老年代上标记 + 整理
ParNew
- Serial的并行版本
- 面向全堆
- 在新生代上标记 + 复制
- 在老年代上标记 + 整理
Parallel Scavenge
- 面向新生代
- 追求提高吞吐量,也就是追求降低处理垃圾的时间在总运行时间中的比例
Serial old
- 面向老年代
- Serial的老年代版本
Parallel old
- 面向老年代
- Parallel 的老年代版本
CMS
- 目标是获取最短停顿时间
- 面向老年代
- 在老年代上标记 + 清除
- 使用标记 + 清除,注定会产生碎片
- 无法处理浮动垃圾,在并发标记和并发清除阶段还是会有新的垃圾产生,只能在下一次垃圾回收的时候处理
- 只能清理老年代,如果想要清理全堆,需要和Serial或ParNew配合
- 处理过程
- 初始标记(STOP THE WORLD)只标记和GC ROOT 直接相关的对象,速度很快
- 并发标记(与用户线程并发)顺着GC ROOT遍历所有相连对象
- 重新标记(STOP THE WORLD)重新标记并发标记过程中出现变动的对象
- 并发清除(与用户线程并发)采用标记+清除,只清除垃圾对象,可以和用户线程并发
G1
- 停顿时间模型:“在n秒的时间里,垃圾回收时间不超过m秒”
- 不再是简单地把内存分成固定大小的新生代老年代,而是分成一个个内存块,每个内存块都可以是新生代,也可以是老年代
- 面向全堆
- 标记 + 整理
- 处理过程
- 初始标记(STOP THE WORLD)只标记和GC ROOT 直接相关的对象,速度很快
- 并发标记(与用户线程并发)顺着GC ROOT遍历所有相连对象
- 重新标记(STOP THE WORLD)标记并发过程中产生的新的垃圾对象
- 并发清除(与用户线程并发)采用标记+整理,清理垃圾,并且移动活对象进行整理,所以不能和用户线程并发,只能STOP THE WORLD
触发初始化
- new关键字实例化对象
- 调用类的静态方法
- 访问类的静态变量
- 反射
- 获取类的class对象
- 获取类的字段
- 调用类的方法
- 初始化子类的时候,如果父类没有初始化,要先初始化父类
- 虚拟机启动时 main所在的类会首先初始化
GC ROOT
- 栈帧中的引用
指向的对象
- 类的静态变量
指向的对象
- 常量池中
引用的对象
- synchronized
锁住的对象
获取class对象的三种方法
- Class.forName("类的全限定名")
- 类名.class
- 对象.getclass()
Class<?> c1 = Class.forName("Account");
System.out.println(c1.getName());
Class<?> c2 = Bank.class;
System.out.println(c2.getName());
Singleton a = Singleton.getInstance();
Class<?> c3 = a.getClass();
System.out.println(c3.getName());
输出
Account
Bank
Singleton
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义