1.gin框架原理详解(gin框架是源码什么)
2.BoltDB源码解析(七)Put和Delete操作
gin框架原理详解(gin框架是什么)
Gin的启动过程、路由及上下文源码解读
Engine是解析gin框架的一个实例,它包含了多路复用器、源码中间件和配置中心。解析
gin通过Engine.Run(addr...string)来启动服务,源码最终调用的解析分时套利指标源码是/手败gin-gonic/gin
一个简单的例子:
packagemain
import"github.com/gin-gonic/gin"
funcmain(){
//Default返回一个默认的路由引擎
r:=gin.Default()
r.GET("/ping",func(c*gin.Context){
//输出json结果给调用方
c.JSON(,gin.H{
"message":"pong",
})
})
r.Run()//listenandserveon0.0.0.0:
}
编译运行程序,打开浏览器,源码访问页面显示:
{ "message":"pong"}
gin的解析功能不只是简单输出Json数据。它是源码一个轻量级的WEB框架,支持RestFull风格API,解析支持GET,源码POST,解析PUT,源码PATCH,解析DELETE,源码OPTIONS等/gin-gonic/gin"
)
funcmain(){
router:=gin.Default()
//静态资源加载,本例为css,js以及资源
router.StaticFS("/public",/ffhelicopter/tmm/website/static"))
router.StaticFile("/favicon.ico","./resources/favicon.ico")
//Listenandserveon0.0.0.0:
router.Run(":")
}
首先需要是生成一个Engine,这是gin的核心,默认带有Logger和Recovery两个中间件。
router:=gin.Default()
StaticFile是加载单个文件,而StaticFS是加载一个完整的目录资源:
func(group*RouterGroup)StaticFile(relativePath,filepathstring)IRoutes
func(group*RouterGroup)StaticFS(relativePathstring,fs/gin-gonic/gin
如果安装失败,直接去Githubclone下来,放置到对应的目录即可。
(2)代码中使用:
下面是一个使用Gin的简单例子:
packagemain
import(
"github.com/gin-gonic/gin"
)
funcmain(){
router:=gin.Default()
router.GET("/ping",func(c*gin.Context){
c.JSON(,gin.H{
"message":"pong",
})
})
router.Run(":")//listenandserveon0.0.0.0:
}
简单几行代码,就能实现一个web服务。使用gin的Default方法创建一个路由handler。然后通过HTTP方法绑定路由规则和路由函数。不同于net/e"}。
注:Gin还包含更多的汇编转换器源码返回方法如c.String,c.HTML,c.XML等,请自行了解。可以方便的返回HTML数据
我们在之前的组v1路由下新定义一个路由:
下面我们访问
可以看到,通过c.Param(“key”)方法,Gin成功捕获了url请求路径中的参数。同理,gin也可以捕获常规参数,如下代码所示:
在浏览器输入以下代码:
通过c.Query(“key”)可以成功接收到url参数,c.DefaultQuery在参数不存在的情况下,会由其默认值代替。
我们还可以为Gin定义一些默认路由:
这时候,我们访问一个不存在的页面:
返回如下所示:
下面我们测试在Gin里面使用Post
在测试端输入:
附带发送的数据,测试即可。记住需要使用POST方法.
继续修改,将PostHandler的函数修改如下
测试工具输入:
发送的内容输入:
返回结果如下:
备注:此处需要指定Content-Type为application/x-www-form-urlencoded,否则识别不出来。
一定要选择对应的PUT或者DELETE方法。
Gin框架快速的创建路由
能够方便的创建分组
支持url正则表达式
支持参数查找(c.Paramc.Queryc.PostForm)
请求方法精准匹配
支持处理
快速的返回给客户端数据,常用的c.Stringc.JSONc.Data
BoltDB源码解析(七)Put和Delete操作
Put和Delete的实现
上一篇文章我们了解了BoltDB的Get API的实现。现在,我们来探讨Put和Delete API的实现:
Put API的主要功能是将一对键值对插入到Bucket中,如果键已经存在,则更新对应的值。首先,进行一些限制条件的检查,例如Put操作是否由写事务发起的,因为Put只能由写事务调用。此外,还需要检查键和值的青春期空间源码大小是否符合限制条件。需要注意的是,Put操作和Get操作一样,这里也使用了Cursor来定位键应该放置的位置。
在实际的Put操作中,会调用Cursor的一个不显眼的方法:
这个方法实际上非常有用,它从当前Bucket的B-tree的根节点开始,一直到Cursor定位到的leaf page,为每个page创建一个对应的node结构。当然,如果一个page已经有对应的node,就直接使用它。
为什么要这么做呢?这是因为事务篇中提到的修改操作具有“传染性”,修改B-tree的leaf节点会导致从root到leaf的所有page都需要修改,而BoltDB的修改操作都是在page对应的node里进行的,不是直接在page上修改,因此需要为这些page建立node结构。具体建立node结构的是Bucket的node方法:
Bucket的node方法有两处需要注意,一个是新建的node会被追加到parent node的children中,记录下这些修改的node之间的关系,这个children在node持久化时会有用(node.spill方法)。另一个是node的数据是如何从page中读取的,这是由node的read方法完成的。
node建立好之后,就在要修改的leaf对应的node上调用put方法:
node的put方法相对简单,它是在inodes数组上查找对应的位置,如果exact为true,表示找到了相同的key,直接更新value;如果exact为false,cm11源码同步相当于找到了应该插入的位置,然后在对应的inode上记录数据。我们来看一下inodes数组的定义:
inodes数组是node实际存储数据的地方,由多个inode组成,每个不同的key对应一个不同的inode,inode之间是按key排序的。对于leaf节点来说,inode里使用key和value;对于branch节点来说,inode里使用key和pgid,pgid代表一个child page的id。value和pgid不会同时使用。
put方法结束后,当前的Put操作也就结束了。也就是说,Put操作所做的仅仅是把新增或修改的数据放入到它所在的page对应的node内存中。
顺便提一下Delete操作,它和Put操作非常类似,在建立起node结构之后,在对应的node的inodes数组中删除找到的key相等的inode就完成了,这里不再展开。
那么,什么时候会把这些node里的数据持久化到DB文件里呢?是在整个写事务commit的时候。
事务的Commit实现
下面是事务commit的代码简化,保留了重要部分:
Commit的整体流程比较长,下面一点一点进行说明。
tx.root.rebalance(),这个root是root Bucket,rebalance是asp.net源码保护对root Bucket下所有子Bucket的所有node进行rebalance。这是什么意思?注意node的初始数据虽然来自一个page,但在经历了一些Delete操作后,有些node里面的数据可能过少,这时会先把这个node和它的左兄弟或右兄弟node合并(node的rebalance方法),合并后node数会减少,但不存在node里数据过少的情况。这个操作对应于B-tree的merge操作,只不过这些node都是Go的内存结构,合并起来非常简单。当然,合并后把这些node spill到page的操作,需要的page总数也会减少。
tx.root.spill(),这个方法是把root Bucket下所有子Bucket的所有node的内容都写入这个事务分配的dirty page里。注意这些dirty page是这个事务临时分配在内存里的,结构和DB文件的page完全一样,但还不是mmap映射的DB的page。
刚开始看到spill这个方法时,感觉它代价有些高,感觉像是把整个B-tree都走了一遍。后来仔细看才发现不是这么回事。这个spill只对有node结构的节点进行处理,那些没修改过的page没有对应的node,根本不会处理。
注意在经过多次Put操作后,node里存放的数据可能出现一个page写不下的情况,比如insert了几千个key value。spill会先把这样的node split成多个大小合适的node(node的split方法),然后把这些node分别写入不同的page中。这个操作对应于B-tree的split操作。和rebalance方法类似的道理,因为这些node都是Go的内存结构,split起来非常容易。
if tx.meta.pgid > opgid,这个判断是看当前事务需要的page数是否大于事务执行前DB文件有的page数,如果大于,说明DB文件放不下了,就调用db.grow增大文件,以容纳新增的page。
紧接着是freelist的持久化操作,因为写事务可能使用了freelist里的一些page,同时也可能释放了一些page到freelist里,所以freelist很可能发生了变化,需要持久化。
tx.write(),这个方法就是把所有的临时分配的dirty page都写入DB文件对应的page里。
tx.writeMeta(),这个方法是把这个tx里的meta写到meta0或者meta1里面(写事务会交替写这两个meta page,这也是个常用技术,叫ping-pong buffer)。它的代码值得看一下:
首先把meta写到临时分配的buf里,然后用文件IO写到DB文件里,最后调用fdatasync,把OS文件的buffer cache持久化到磁盘上。至此,写事务的所有数据都已经落盘完毕。后面新开启的事务会因为这个meta的txid是最大的,而选择使用这个最新的meta page。而这个meta page包括最新的root bucket,最新的freelist,最新的pgid,这些总体构成了一个DB的最新版本,保证新开启的事务读到最新版本的数据。
看tx.write()和tx.writeMeta()的实现可以发现,写入数据用的是db.ops.writeAt,而这个方法默认值就是File.WriteAt方法,所以实际写入文件用的是文件IO,而不是直接写mmap内存。而BoltDB使用mmap一开始就把mmap映射的内存标记为只读的,压根不允许直接写mmap内存。为什么要这么做呢?
猜测可能是为了安全。前面讲到Get操作为了性能是zero copy的,发现Get返回来的value是mmap上数据的指针,如果mmap设置为可读写的,应用程序代码五花八门,可能会通过指针一不小心修改了mmap上的数据,这样的修改因为走的不是API是无法保证事务的。把mmap设置为只读的消除了这种可能性。反过来说,如果mmap设置为可读写的,Get就不能返回mmap上的指针了,为了安全一定要copy一份数据出来才行,降低了Get的性能。
这里还有个很自然而且很重要的问题是,如果事务commit失败了呢,BoltDB如何保证事务的原子性(ACID的A),确保这个写事务的所有操作,不论是落盘的,还是没落盘的,都不会生效?
原子性要求,不管是commit走到哪一步,哪怕是已经把修改的数据,甚至包括修改的freelist已经落盘,只要最终事务commit失败,都不能对正确性产生任何影响。这里的正确性是指,数据库的状态(有实际的key value数据,freelist, pgid等共同构成)必须是在这个写事务运行之前的状态,数据不能被破坏,这个写事务也不能留下可被后续事务读到的任何更新。
要做到原子性貌似挺难的,因为事务的commit里包括很多步骤,这些步骤都不是原子性的。不过重要的一点是,不论commit运行到哪一步,因为tx.writeMeta是最后一步,只有这一步运行成功commit才算成功,如果说commit失败了,那么tx.writeMeta一定是没运行,或者运行了半截,这个meta page没写完整,机器断电了。总之,这些情况下我们不会得到一个合法的新的meta page(这种情况下meta的validate方法会失败,因为meta的checksum不对)。这时候ping-pong buffer的meta page就起重要的作用了,因为交替写meta page的原因,即使这个写事务新的meta page没写成功,这个写事务运行前版本的meta page还在,而这个meta page包括这个写事务运行前的DB版本所有的状态(kv数据,freelist,pgid等)。这个meta page会被后续事务使用,就像那个失败的写事务从来没有运行过一样。而那个写事务留下的kv数据的page,freelist的page,即使是持久化了,也因为没有写成新的meta,没有机会被用到。
还有个自然的疑问,即使这个失败的写事务写的page因为没有合法的meta无法被引用,不会影响正确性,但无法被引用是不是也意味着这些page无法被回收,浪费了磁盘空间?
答案是也不会。在原来版本的meta里的free list和pgid的共同作用下,这些page会被视为free的,还可以使用,不会出现无法回收这些page的情况。
还有个疑问,既然BoltDB交替写meta0和meta1,是不是连续两个事务commit正好在写meta时失败,数据库就废了?
仔细研究发现,还是没事!因为写事务的txid也是meta的一部分,一个写事务失败,导致txid不会增长,下一次写事务的txid还是一样,meta的交替写是因为txid的变化引起的,既然没变化,就不交替了。所以下一个写事务即使写meta还失败了,也还是写的上一个写事务写的那个meta,不会把两个meta都写坏。
总结一下,ping-pong buffer的meta page真是设计得精巧,是BoltDB达到原子性的关键!
2024-11-29 20:30
2024-11-29 20:14
2024-11-29 18:34
2024-11-29 18:23
2024-11-29 18:12
2024-11-29 17:53