接着上一篇的繼續,
使用go實現一個適用於嵌入式上的存儲模塊。簡單易用,使用簡單方便。
由於在終端上,不需要執行復雜的sql查詢,多表級聯查詢等。就是用來存儲記錄的,因此設計爲存儲到表裏的都爲二進制的字節流。
還有一個原因是終端上記錄字段變動頻繁,不適合動不動就更改數據庫的表結構吧。如果想要方便記錄的解析,可以結合protobuf把數據序列化爲字節流存儲進去。
以下爲按照這個思路的實現的存儲方案:
首先記錄分爲若干區,底層實現對應若干個表,表裏存儲記錄。
然後在單獨建一個表,作爲目錄表。目錄表裏只有若干條記錄。注意這個表不是用來插入數據的。是用來記錄記錄表裏的記錄寫到什麼位置了,上傳到什麼位置了。
表的結構如下:
如圖所示,tb_dir目錄表裏只有10條數據。不會增也不會減,只會更新。分別對應tb_rec01---tb_rec10這十個記錄表。
記錄了當前的記錄流水號,當前記錄寫的位置,當前記錄讀的位置等信息。
再看下記錄表裏有哪些內容。
分別有id,recNo(記錄流水),
recType(記錄類型),
recTime(記錄時間),
data(記錄二進制數據內容,比byte字節流,長度不限),
ext(預留擴展),res(預留)
操作有哪些接口?
都在recapi.go文件中,
package sqllite
// 配置項
const (
// MAXRECDIRS 最大記錄目錄數量
//(一個記錄目錄對應控制一個記錄表,它記錄了記錄表中的數據存儲和讀取的位置)
MAXRECDIRS = 10
// MAXRECAREAS 最大記錄區數量 10個(即記錄表的個數,必須跟記錄目錄數量保持一致)
MAXRECAREAS = MAXRECDIRS
// MAXRECCOUNTS 最大記錄條數(即一個表中允許存儲的最大記錄數,例100000條
// 記錄存滿後且上傳標記已清除後,則從頭開始存儲覆蓋,存儲一條,覆蓋一條)
MAXRECCOUNTS = 100000
)
//枚舉,記錄區定義(一個記錄區對應一個表)
const (
//RecArea01 記錄區1
RecArea01 = iota + 1
//RecArea02 記錄區2
RecArea02
//RecArea03 記錄區3
RecArea03
//RecArea04 記錄區4
RecArea04
//RecArea05 記錄區5
RecArea05
//RecArea06 記錄區6
RecArea06
//RecArea07 記錄區7
RecArea07
//RecArea08 記錄區8
RecArea08
//RecArea09 記錄區9
RecArea09
//RecArea10 記錄區10
RecArea10
)
// Recorder 操作記錄的接口聲明
type Recorder interface {
// 初始化記錄區(會清空所有數據!)
InitRecAreas() error
// 打開記錄區(開機必須先打開一次)
OpenRecAreas() (err error)
// 保存記錄
SaveRec(areaID int, buf []byte, recType int) (id int64, err error)
// 刪除記錄
DeleteRec(areaID int, num int64) (err error)
// 獲取未上傳記錄數量
GetNoUploadNum(areaID int) int
// 按數據庫ID讀取一條記錄
ReadRecByID(areaID int, id int) (p *Records, err error)
// 順序讀取未上傳的記錄
ReadRecNotServer(areaID int, sn int) (p *Records, err error)
// 倒數讀取記錄(如sn=1代表最後一次寫入的記錄)
ReadRecWriteNot(areaID int, sn int) (p *Records, err error)
// 最後一條記錄流水
GetLastRecNO(areaID int) int
}
// RecAPI 操作接口的類
type RecAPI struct {
Recorder
}
// NewRecAPI 初始化操作接口
func NewRecAPI(debug bool) RecAPI {
return RecAPI{Recorder: NewRecords(debug)}
}
如何使用?代碼存放在我的github,地址:https://github.com/yongzhena/go-sqllite
有個demo, main.go
package main
import (
"log"
rec "github.com/yongzhena/go-sqllite"
)
func checkErr(err error) {
if err != nil {
panic(err)
}
}
func main() {
log.Println("test sqllite...")
log.Println("InitRecAreas...")
opt := rec.NewRecAPI(true)
err := opt.InitRecAreas()
if err != nil {
log.Fatal(err.Error())
}
log.Println("InitRecAreas ok!")
err = opt.OpenRecAreas()
if err != nil {
log.Fatal(err.Error())
}
log.Println("OpenRecAreas ok!")
id, err := opt.SaveRec(1, []byte("123456789011"), 0)
if err != nil {
log.Println(err.Error())
}
log.Printf("over,SaveRec ok!,area=%d,id=%d\n", 1, id)
id, err = opt.SaveRec(1, []byte("1234567890221111"), 1)
if err != nil {
log.Println(err.Error())
}
id, err = opt.SaveRec(2, []byte("123456789022"), 1)
if err != nil {
log.Println(err.Error())
}
log.Printf("over,SaveRec ok!,area=%d,id=%d\n", 2, id)
id, err = opt.SaveRec(2, []byte("123456789022"), 3)
if err != nil {
log.Println(err.Error())
}
log.Printf("over,SaveRec ok!,area=%d,id=%d\n", 2, id)
num := opt.GetNoUploadNum(1)
log.Printf("area=%d,NoUploadNum=%d\n", 1, num)
recp, err := opt.ReadRecNotServer(1, 1)
if err != nil {
log.Fatal(err.Error())
}
log.Println(recp)
err = opt.DeleteRec(1, 1)
if err != nil {
log.Fatal(err.Error())
}
num = opt.GetNoUploadNum(1)
log.Printf("area=%d,NoUploadNum=%d\n", 1, num)
num = opt.GetNoUploadNum(2)
log.Printf("area=%d,NoUploadNum=%d\n", 2, num)
}
是不是很簡單?完成了記錄存儲和記錄獲取。完全看不到任何sql的影子。
記錄裏有日期和流水和記錄類型等簡單信息供查詢。
記錄的內容爲二進制byte流,想存什麼就存什麼,存多長也無所謂。至於解析記錄嘛,建議結合protobuf來用。
把記錄序列化後存儲進去。
這幾個接口,在嵌入式終端上足夠用了。可以寫入記錄,按順序讀取未上傳記錄。刪除記錄(並非真正的刪除記錄,若直接刪記錄在終端上是不安全的。而是改了目錄表裏的readID。並且記錄存儲滿後會從頭覆蓋。前提是該記錄已上傳完畢。是不是很安全?這在終端上操作是必須要考慮的。不能讓表裏記錄一直存下去,得指定大小。存滿了也不能刪,得從頭一條條覆蓋。)
這種思路是否成熟?
我們原來的c代碼,單片機的應用,就是這麼做的。只不過底層操作的是Flash。
以下爲內部實現:
recdir的實現:
package sqllite
import (
"errors"
"fmt"
"log"
db "sqllite/database"
)
// RecDir ...
type RecDir struct {
ID int `json:"id"`
RecNo int `json:"recno" `
WriteID int64 `json:"writeid" `
ReadID1 int64 `json:"readid1" `
ReadID2 int64 `json:"readid2" `
ReadID3 int64 `json:"readid3" `
Rp int `json:"rp" `
Res int `json:"res" `
Flag bool `json:"flag" `
}
// InitRecDir ...
func InitRecDir() (err error) {
//創建表
sqlTable := `
DROP TABLE IF EXISTS tb_dir;
CREATE TABLE IF NOT EXISTS tb_dir(
id INTEGER PRIMARY KEY AUTOINCREMENT,
recNo INTEGER NOT NULL,
writeID INTEGER NOT NULL,
readID1 INTEGER NOT NULL,
readID2 INTEGER ,
readID3 INTEGER ,
rp INTEGER ,
res INTEGER
);
`
if db.SQLDB == nil {
err = errors.New("db.SQLDB is null")
log.Fatal(err.Error())
return
}
log.Println("begin create dir table...")
if IsDebug {
log.Println("sql:" + sqlTable)
}
_, err = db.SQLDB.Exec(sqlTable)
if err != nil {
log.Fatal(err.Error())
}
log.Println("create dir table ok!")
//清空數據
// log.Println("begin truncate dir table...")
// _, err = db.SQLDB.Exec(`UPDATE sqlite_sequence SET seq = 0 WHERE name = 'tb_dir' `)
// if err != nil {
// log.Fatal(err.Error())
// }
// log.Println("truncate dir table ok!")
log.Println("begin init dir table...")
for i := 0; i < MAXRECDIRS; i++ {
_, err = db.SQLDB.Exec("INSERT INTO tb_dir(recNo, writeID,readID1,readID2,readID3,rp,res) VALUES (?, ?,?,?,?,?,?)", 0, 0, 0, 0, 0, 0, 0)
if err != nil {
log.Fatal(err.Error())
}
}
log.Println("init dir table ok!")
return err
}
// UpdateDirs 更新目錄
func (rd *RecDir) UpdateDirs(areaID int) error {
strSQL := fmt.Sprintf("UPDATE tb_dir SET recNo=%d, writeID=%d, readID1=%d, readID2=%d, readID3=%d, rp=%d,res=%d WHERE id=%d",
rd.RecNo, rd.WriteID, rd.ReadID1, rd.ReadID2, rd.ReadID3, rd.Rp, rd.Res, areaID)
if IsDebug {
log.Println(strSQL)
}
res, err := db.SQLDB.Exec(strSQL)
if err != nil {
log.Fatal(err.Error())
}
affect, err := res.RowsAffected()
fmt.Println(affect)
if IsDebug {
log.Println(rd)
}
return err
}
// LoadDirs 加載(讀取)目錄
func (rd *RecDir) LoadDirs(areaID int) error {
strSQL := fmt.Sprintf("SELECT * FROM tb_dir WHERE id=%d", areaID)
if IsDebug {
log.Println(strSQL)
}
rows, err := db.SQLDB.Query(strSQL)
if err != nil {
log.Fatal(err.Error())
return err
}
if rows.Next() {
err = rows.Scan(&rd.ID, &rd.RecNo, &rd.WriteID, &rd.ReadID1, &rd.ReadID2, &rd.ReadID3, &rd.Rp, &rd.Res)
if err != nil {
log.Fatal(err.Error())
return err
}
} else {
log.Fatal("no dir records")
}
rows.Close()
if IsDebug {
log.Println(rd)
}
return err
}
records.go實現:
package sqllite
import (
"errors"
"fmt"
"log"
db "sqllite/database"
"strings"
"time"
)
var (
//IsDebug 是否調試
IsDebug = true
recDir [MAXRECAREAS]RecDir
)
// Records ...
type Records struct {
ID int `json:"id"`
RecNo int `json:"recno" `
RecType int `json:"rectype" `
RecTime string `json:"rectime" `
Data []byte `json:"data" `
Ext string `json:"ext" `
Res string `json:"res" `
}
// InitRecAreas 初始化記錄存儲區
func (rec Records) InitRecAreas() error {
//初始化目錄表
err := InitRecDir()
if err != nil {
log.Fatal(err.Error())
return err
}
//創建記錄表
sqlTable := `
DROP TABLE IF EXISTS TB_NAME;
CREATE TABLE IF NOT EXISTS TB_NAME (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recNo INTEGER NOT NULL,
recType INTEGER NOT NULL,
recTime INTEGER NOT NULL,
data BLOB ,
ext TEXT ,
res TEXT
);
`
for i := 0; i < MAXRECAREAS; i++ {
tbName := fmt.Sprintf("tb_rec%02d", i+1)
log.Println("begin create rec table " + tbName)
sqls := strings.Replace(sqlTable, "TB_NAME", tbName, -1)
if IsDebug {
log.Println("sql:" + sqls)
}
_, err = db.SQLDB.Exec(sqls)
if err != nil {
log.Fatal(err.Error())
return err
}
log.Println("create rec table " + tbName + " ok!")
}
return err
}
// OpenRecAreas 打開記錄存儲區,每次開機,需要先打開一下
func (rec Records) OpenRecAreas() (err error) {
//加載RecDir
for i := 0; i < MAXRECAREAS; i++ {
log.Printf("LoadDirs %02d \n", i+1)
err = recDir[i].LoadDirs(i + 1)
if err != nil {
log.Println(err.Error())
return
}
log.Printf("LoadDirs %02d ok!\n", i+1)
}
//log.Println(recDir)
return err
}
// SaveRec 保存記錄
func (rec *Records) SaveRec(areaID int, buf []byte, recType int) (id int64, err error) {
log.Printf("SaveRec,area=%02d \n", areaID)
if (areaID <= 0) || (areaID > MAXRECAREAS) {
err = fmt.Errorf("area id %02d is not right,mast between 1 and %02d", areaID, MAXRECAREAS)
log.Println(err.Error())
return
}
rec.RecNo = recDir[areaID-1].RecNo
t := time.Now()
rec.RecTime = t.Format("20060102150405")
rec.Data = buf
rec.RecType = recType
//記錄是否存儲滿,判斷
if (recDir[areaID-1].WriteID + 1) > (int64)(MAXRECCOUNTS) {
if recDir[areaID-1].ReadID1 == 0 {
err = fmt.Errorf("rec area %02d is full", areaID)
log.Println(err.Error())
return
}
if (recDir[areaID-1].WriteID + 1 - int64(MAXRECCOUNTS)) == recDir[areaID-1].ReadID1 {
err = fmt.Errorf("rec area %02d is full", areaID)
log.Println(err.Error())
return
}
//保存記錄
strSQL := fmt.Sprintf(`UPDATE tb_rec%02x SET recNo=%d, recType=%d,recTime=%s,data=?,ext="%s",res="%s" WHERE id = 1`,
areaID, rec.RecNo+1, rec.RecType, rec.RecTime, rec.Ext, rec.Res)
if IsDebug {
log.Println(strSQL)
}
_, err = db.SQLDB.Exec(strSQL, rec.Data)
if err != nil {
log.Fatal(err.Error())
return
}
recDir[areaID-1].RecNo++
recDir[areaID-1].WriteID = 1
recDir[areaID-1].Flag = true
id = 1
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return
}
log.Printf("SaveRec,area=%02d ok!\n", areaID)
return id, err
}
if recDir[areaID-1].Flag {
//記錄是否滿判斷
if (recDir[areaID-1].WriteID + 1) == recDir[areaID-1].ReadID1 {
err = fmt.Errorf("rec area %02d is full", areaID)
log.Println(err.Error())
return
}
id = recDir[areaID-1].WriteID + 1
strSQL := fmt.Sprintf(`UPDATE tb_rec%02x SET recNo=%d, recType=%d,recTime=%s,data=?,ext="%s",res="%s" WHERE id = %d`,
areaID, rec.RecNo+1, rec.RecType, rec.RecTime, rec.Ext, rec.Res, id)
if IsDebug {
log.Println(strSQL)
}
_, err = db.SQLDB.Exec(strSQL, rec.Data)
if err != nil {
log.Fatal(err.Error())
return
}
recDir[areaID-1].RecNo++
recDir[areaID-1].WriteID = id
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return 0, err
}
log.Printf("SaveRec,area=%02d ok!\n", areaID)
return id, err
}
strSQL := fmt.Sprintf(`INSERT INTO tb_rec%02x(recNo, recType,recTime,data,ext,res) VALUES (%d,%d,%s,?,"%s","%s")`,
areaID, rec.RecNo+1, rec.RecType, rec.RecTime, rec.Ext, rec.Res)
if IsDebug {
log.Println(strSQL)
}
rs, err := db.SQLDB.Exec(strSQL, rec.Data)
if err != nil {
log.Fatal(err.Error())
return 0, err
}
id, err = rs.LastInsertId()
if err != nil {
log.Fatal(err.Error())
return 0, err
}
recDir[areaID-1].RecNo++
recDir[areaID-1].WriteID = id
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return 0, err
}
log.Printf("SaveRec,area=%02d ok!\n", areaID)
return id, err
}
// DeleteRec 刪除記錄(並不是真正刪除表裏記錄,而是清除該記錄的上傳標記)
// areaID:記錄區 num:刪除的數量
func (rec Records) DeleteRec(areaID int, num int64) (err error) {
if (areaID <= 0) || (areaID > MAXRECAREAS) {
err = errors.New("area id is not right")
log.Fatal(err.Error())
return
}
id := recDir[areaID-1].ReadID1
//如果寫的位置等於讀的位置,說明記錄已上傳完,沒有要刪除的了
if recDir[areaID-1].WriteID == recDir[areaID-1].ReadID1 {
return
}
//如果要刪除的數量大於了最大的記錄數
if (id + num) > MAXRECCOUNTS {
if (id + num - MAXRECCOUNTS) > recDir[areaID-1].WriteID {
recDir[areaID-1].ReadID1 = recDir[areaID-1].WriteID
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return err
}
return
}
//更新讀指針(讀的位置)
recDir[areaID-1].ReadID1 = id + num - MAXRECCOUNTS
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return err
}
return
}
//如果當前寫的位置大於讀的位置
if recDir[areaID-1].WriteID > recDir[areaID-1].ReadID1 {
if id+num > recDir[areaID-1].WriteID {
//更新讀指針(讀的位置)
recDir[areaID-1].ReadID1 = recDir[areaID-1].WriteID
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return err
}
return
}
}
//更新讀指針(讀的位置)
recDir[areaID-1].ReadID1 = id + num
err = recDir[areaID-1].UpdateDirs(areaID)
if err != nil {
log.Fatal(err.Error())
return err
}
return
}
//GetNoUploadNum 獲取未上傳記錄數量
func (rec Records) GetNoUploadNum(areaID int) int {
num := 0
if recDir[areaID-1].WriteID == recDir[areaID-1].ReadID1 {
num = 0
return num
}
if recDir[areaID-1].Flag == false {
num = int(recDir[areaID-1].WriteID - recDir[areaID-1].ReadID1)
} else {
if recDir[areaID-1].WriteID > recDir[areaID-1].ReadID1 {
num = int(recDir[areaID-1].WriteID - recDir[areaID-1].ReadID1)
} else {
num = int(MAXRECCOUNTS - recDir[areaID-1].ReadID1 + recDir[areaID-1].WriteID)
}
}
return num
}
// ReadRecByID 按數據庫ID讀取記錄
func (rec Records) ReadRecByID(areaID int, id int) (p *Records, err error) {
var rec1 Records
if (areaID <= 0) || (areaID > MAXRECAREAS) {
err = errors.New("area id is not right")
log.Fatal(err.Error())
return
}
strSQL := fmt.Sprintf("SELECT * FROM tb_rec%02d WHERE id=%d", areaID, id)
if IsDebug {
log.Println(strSQL)
}
rows, err := db.SQLDB.Query(strSQL)
if err != nil {
log.Fatal(err.Error())
return nil, err
}
if rows.Next() {
err = rows.Scan(&rec1.ID, &rec1.RecNo, &rec1.RecType, &rec1.RecTime, &rec1.Data, &rec1.Ext, &rec1.Res)
if err != nil {
log.Fatal(err.Error())
return nil, err
}
} else {
log.Println("no records")
return nil, err
}
rows.Close()
return &rec1, nil
}
//ReadRecNotServer 讀取未上傳的記錄數據,順序讀取第SN條未上傳的記錄
//sn取值 1-到-->未上傳記錄數目
func (rec Records) ReadRecNotServer(areaID int, sn int) (p *Records, err error) {
if (areaID <= 0) || (areaID > MAXRECAREAS) {
err = errors.New("area id is not right")
log.Fatal(err.Error())
return
}
id := recDir[areaID-1].ReadID1
if (int(id) + sn) > MAXRECCOUNTS {
if int(id)+sn-MAXRECCOUNTS > int(recDir[areaID-1].WriteID) {
return nil, errors.New("no records")
}
p, err = rec.ReadRecByID(areaID, int(id)+sn-MAXRECCOUNTS)
} else {
if recDir[areaID-1].ReadID1 < recDir[areaID-1].WriteID {
if (int(id) + sn) > int(recDir[areaID-1].WriteID) {
return nil, errors.New("no records")
}
p, err = rec.ReadRecByID(areaID, int(recDir[areaID-1].ReadID1)+sn)
}
}
return p, err
}
// ReadRecWriteNot 倒數讀取第SN條寫入的記錄
//讀取一條記錄 倒數讀取第SN條寫入的記錄
func (rec Records) ReadRecWriteNot(areaID int, sn int) (p *Records, err error) {
id := int(recDir[areaID-1].WriteID)
if (id - sn) < 0 {
if recDir[areaID-1].Flag {
p, err = rec.ReadRecByID(areaID, MAXRECCOUNTS-(sn-id-1))
} else {
return nil, errors.New("no records")
}
} else {
p, err = rec.ReadRecByID(areaID, (id - sn + 1))
}
return
}
// GetLastRecNO 獲取最後一條記錄流水號
func (rec Records) GetLastRecNO(areaID int) int {
if (areaID <= 0) || (areaID > MAXRECAREAS) {
log.Println("area id is not right")
return 0
}
id := recDir[areaID-1].RecNo
return id
}
// NewRecords ...
func NewRecords(debug bool) *Records {
IsDebug = debug
records := new(Records)
return records
}