数据存储那点事

1. 序言

从前说到后,数据存储那点事。

在计算机及嵌入式软件中,不可避免的要实现数据存储。

本文从硬件层到软件层,从前台到后台梳理了数据存储的方式和内容。并且会不断更新。欢迎Mark。

目的是在工作中,了解不同数据存储方式的特点,以选择合适的存储方式。以及存储策略。

如果使用英文access, 翻译成中文是存取,即有存又有取。在本文中不区分“存取”和“存储”,认为是同一含义。

另外着重关注java, android等在存储方面的细节。

然而,前后端技术是相通的。做单片机和Android的同学,了解一下后端的知识体系也是OK的,反之也是,不是吗?

2. 计算机基本组成

无论是计算机(PC, Server)还是嵌入式软件,目前的整体架构大多都是冯诺依曼的。

image.png

1.运算器
2.控制器
3.存储器
4.输入设备
5.输出设备

其中,存储器分内部存储和外部存储

内部存储:又称内存,内存条,内存颗粒(IC)
外部存储:又称外存,硬盘,闪存。
本篇讨论的主要是外部存储

3. 外部存储主要的硬件

EEPROM

电可擦除ROM
一般在单片机系统中使用到。
我们知道ROM是Read Only Memory,只能读不能写。但EEPROM是电可擦除的。就是说能读能写。
一般用来存储少量数据.

单片机和EEPROM芯片的通信,一般是用SPI或I2C总线标准。比如这是一款24C02芯片


image.png

当然,其实现在很多EEPROM也被集成到了单片机内部,电路搭建起来更为方便。

以下是2003年写的I2C总线读写程序,可参考https://blog.csdn.net/gsnet/article/details/13887

Flash芯片

我们平常说的Flash,就是U盘,以及固态硬盘上的,一般是这个
每字节成本非常低。由于物理限制,提出了block块概念。擦写是按块的,而不是按字节的。
在遥远的上个世纪,每个块的最大擦写次数为100万次。
但是随着对存储容量的增长,为了平衡每bit的成本,发明了新技术,使得最大擦写次数指数级下降。(什么?技术越发展越差了?)
SLC Single-Level Cell,意味着每个存储单元只存放 1bit讯息,靠浮置闸里电子捕获状态的有或无来输出成数据(即使在 0的状态浮置闸里其实还是有电子,但不多),也就是最简单的 0与1;
可擦写10万次

MLC Multi-Level Cell,意味着每个存储单元可存放 2bit讯息,浮置闸里电子的量会分为高、中、低与无四种状态,转换为二进制后变成 00、01、10、11;
可擦写5000次

TLC Triple-Level Cell ,更进一步将浮置闸里的电子捕获状态分成八种,换算成二进制的 000、001、010、011、100、101、110、111,也就是3bit。
可擦写1000次

Flash芯片照片:


image.png

eMMC, UFS, 固态硬盘

其实都是Flash的变种,是封装了硬件协议和磨损均衡之后的成品。更容易接入我们的嵌入式CPU或者电脑系统

eMMC图片:


eMMC图片

UFS图片:


UFS图片

固态硬盘图片:


固态硬盘图片

机械式硬盘

有盘片,磁头等。


机械式硬盘

4. 软件存储方法

上面讲了硬件基础,下面聊一下软件方法。

1)EEPROM等

使用单片机或CPU, 按字节或block发送SPI或I2C或内部指令,完成读写。

2)简单无格式文本文件

写文件是常见的方式。通常我们会在软件中,定义一个文本文件,例如:1.txt
然后,无论是汇编、C语言、Java(TM)、 python等, 都有直接读写文件的API.
我们对1.txt这样的文本文件常用行作为一个存取单元,行与行之间使用换行符分隔

3) 带格式的文本配置文件

在windows上常使用ini

myconfig.ini
几个概念Section,比如下面的Display
再下面就是key=value这种写法了,很容易

[Display]
Video=wechat_video_scan.mp4,wildlife.mp4,todaynobodysleep.mp4

Image=zhangyu.jpg,hu.jpg,river.png

在Java和Android,可以使用ini4j库
官网是http://ini4j.sourceforge.net/

有时候我们使用xml
xml使用成对的标签来表示级联的关系

    
<books>
    <book>
        <filename>001.pdf</filename>
        <keywords>
            <word>ISO9001标准</word>
            <word>张先生编著</word>
            <word>20190305</word>
        </keywords>
    </book>
    <book>
        <filename>002.pdf</filename>
        <keywords>
            <word>密码学</word>
            <word>李先生编著</word>
            <word>20190508</word>
        </keywords>
    </book>
    <!--....-->
</books>   

有很方便的网页试验工具,可以去写写试
http://www.bejson.com/otherformat/xmlsort/

有时候我们使用更简短的json(从javascript演变而来)

先看看json官网,膜拜一下
http://www.json.org/
JSON就是一串字符串 使用特定的符号来关联起有用的信息。

{} 双括号表示对象

[] 中括号表示数组

"" 双引号内是属性或值

: 冒号表示后者是前者的值(这个值可以是字符串、数字、也可以是另一个数组或对象)

所以 {"name": "Grace"} 可以理解为是一个包含name为Michael的对象

而[{"name": "Grace"},{"name": "John"}]就表示包含两个对象的数组

当然了,你也可以使用{"name":["Grace","John"]}来简化上面一部,这是一个拥有一个name数组的对象

尽可能打上引号,例如
ps:现在还有很多人存在一些误区,为什么{name:'Grace'}在检验时通过不了,
那是因为JSON官网最新规范规定

如果是字符串,那不管是键或值最好都用双引号引起来,所以上面的代码就是{"name":"Grace"}

json例子:

{
    "books": [{
            "filename": "001.pdf",
            "keywords": ["iso9001", "Mr. zhang"]
        },
        {
            "filename": "002.pdf",
            "keywords": ["Thinking in Java", "Bruce"]
        }
    ]
}

在spring boot开发中使用properties或yml 脚本文件

.properties文件格式和yml是对等的, 都用来写配置文件。但是大家都习惯用yml,因为它使用缩进格式,有层次感。

来看一个开源项目thingsboard的yml配置文件示例
缩进格式其实是用来表示嵌套级别的

#
# Copyright © 2016-2019 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


version: '2.2'

services:
  zookeeper:
    restart: always
    image: "zookeeper:3.5"
    ports:
      - "2181"
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=zookeeper:2888:3888;zookeeper:2181
  kafka:
    restart: always
    image: "wurstmeister/kafka"
    ports:
      - "9092:9092"
    env_file:
      - kafka.env
    depends_on:
      - zookeeper
  redis:
    restart: always
    image: redis:4.0
    ports:
      - "6379"

平时我们用,照猫画虎就可以了。至于想学更多yml, 还是看阮一峰的博客吧 http://www.ruanyifeng.com/blog/2016/07/yaml.html

如果配置简单的key-value写文件,还是ini比较方便。但有时候如果复杂些,且经常配置修改。需要层次感,可以考虑用yml
https://www.cnblogs.com/yihuihui/p/9200790.html

4) 单机的关系型数据库

一般没有选择了,如果说回到上个世纪90年代,我们还用过一种叫dBaseIII的DOS界面的单机数据库。以及偶尔在windows上有人用access数据库。

那到了2019年的今天,sqlite几乎成为了一致的通用选择了。
SQLite有多强大的影响呢?
IOS苹果平台底用使用sqlite
Android自带SQLite
Windwos上可以使用SQLite
Linux系上可以使用SQLite

SQLite教程很多,这里不多介绍.介绍一个python使用sqlite3的例子:


import sqlite3
conn = sqlite3.connect('test.db')
cursor = conn.cursor()

cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')

cursor.execute('insert into user(id, name) values (\'1\', \'Stephen\')' )
cursor.execute('insert into user(id, name) values (\'2\', \'Grace\')' )

results = cursor.execute('select id,name from user')
for row in results:
    print('id=', row[0])
    print('id=', row[1])

cursor.close()
conn.commit()
conn.close()

开发环境使用visual studio code, run后的结果:

[Running] python -u "e:\mydocu\pythonwork\mysqlitetest.py"
id= 1
id= Stephen
id= 2
id= Grace

[Done] exited with code=0 in 0.483 seconds

使用起来很简单

5) 后台常用关系型数据库

最常用的关系型数据库MySql

免费,易用
如果要用在centos上,可以用它的替换者MariaDB,几乎指令和使用方式是一样的。

关于sql link访问

是的,如果客户端要访问MySql 及SQLServer等。可以直接通过库访问。听上去很帅吧,不用开发中间服务器接口。but-要考虑安全问题,真的确定要把生产环境的密码存在客户端吗?
然而,真的得知身边有几个项目是这样做的。

mysql 比较完整的教程:
https://www.runoob.com/mysql/mysql-tutorial.html

6) 对象数据库

image.png
image.png

对象数据库的例子: MongoDB

来自于英文单词“Humongous”,中文含义为“庞大”(不是芒果mango,你英语老师该为你难过了)

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

SQL术语 MongoDB术语 解释
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB不支持
primary key primary key 主键,MongoDB自动将_id字段设置为主键

简单说,每条关系型数据库的记录,就是个json对象,json里面每个key:value中的key对应表头字段名,value对应记录中此字段的值。

试试创建一个mongo库并插入一条数据

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
> use testdb //这就是创建
switched to db testdb
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
> db
testdb
> db.createCollection("student")
{ "ok" : 1 }
> show collections
student
> show tables //看见没有,很人性化,知道我们会把collections误叫table
student
> db.student.insert({"name":"Grace"}) //插入一条数据
WriteResult({ "nInserted" : 1 })
> db.student.insert({"name":"Stephen"}) //再插入一条数据
WriteResult({ "nInserted" : 1 })

> db.student.find() //查看一下集合(表)里有什么
{ "_id" : ObjectId("5d40f9a29b54f98349681b38"), "name" : "Grace" }
{ "_id" : ObjectId("5d40fa379b54f98349681b39"), "name" : "Stephen" }

> db.student.find({"name":"Stephen"}).pretty() //像是sql里面的where条件查询
{ "_id" : ObjectId("5d40fa379b54f98349681b39"), "name" : "Stephen" }

在Spring boot中怎么操控mongo呢,看这篇文章https://blog.csdn.net/qq_33619378/article/details/81544711

基本流程仍然是:
yml配置连接mongo参数 -> 定义Bean并使用mongo注解 -> 封装Service 和Dao

7)时序数据库TSDB

TSDB概述

按照维基百科解释,时间序列数据库(TSDB)是一个为了用于处理时间序列数据而优化的软件系统,其按时间数值或时间范围进行索引。

写操作的频率远远大于读,并且存在一定的时间窗口。此外,时序数据库通常极少更新数据,存在一定的覆盖写,并且支持批量删除操作。

时序数据库经常用于以下类似场景:

来看排名前10位的TSDB榜单:


image.png

选型哪个好呢? 网上讨论InfluxDB和OpenTSDB的比较多。
InfluxDB性能和节约空间最强,不过集群部分现在不开源了。所以有人转投OpenTSDB等。

排名第一的时序数据库InfluxDB介绍

influxDB中的名词 传统数据库中的概念
database 数据库
measurement 数据库中的表
points 表里面的一行数据
InfluxDB中独有的概念:
  • Point
    Point由时间戳(time)、数据(field)、标签(tags)组成。

Point相当于传统数据库里的一行数据:

Point属性 传统数据库中的概念
time 每个数据记录时间,是数据库中的主索引(会自动生成)
fields 各种记录值(没有索引的属性)也就是记录的值:温度, 湿度
tags 各种有索引的属性:地区,海拔
  • series
    所有在数据库中的数据,都需要通过图表来展示,而这个series表示这个表里面的数据,可以在图表上画成几条线:通过tags排列组合算出来。

对InfluxDB时序数据库的原理分析
https://segmentfault.com/a/1190000005977485

来看一下例子:

> create database tsdb666 //创建数据库
> use tsdb666 //打开数据库
Using database tsdb666

//measurement或称table不用定义,直接插入就行,例如stock这个measurement,我们是从来没定义过的
//插入今天的股价
> INSERT stock,code=601668,name="中国建筑" price=5.89
> INSERT stock,code=600015,name="华夏银行" price=7.56
> 
> select * from stock //select可以用来查询,和关系数据库好象
name: stock
-----------
time            code    name    price
1564559969067570478 601668  "中国建筑"  5.89
1564560004602845932 600015  "华夏银行"  7.56

很有趣的是,官方不提供update的方法,对delete貌似也不支持(反正我删除没成功)。网上很多文章也在说这个。
对于过期的数据,推荐使用“保留策略RP”来清除。
保留策略,即数据的过期策略:如 CREATE RETENTION POLICY "a_year" ON "food_data" DURATION 52w REPLICATION 1 default
这个语句对数据库 food_data 创建了一个叫做 a_year 的RP, a_year 保存数据的周期是52周

tags 和 time 是判断时序数据库点(point)的唯一性的标准。相当于关系型数据库的复合主键。所以当我们insert的time和tags 跟原有的数据重复时,就会覆盖掉原有数据的 field value

下面就是使用insert来实现类似update功能的铁证

> delete from stock where code=600015 //想删除600015这个股票价格,可是没指明time,当然不能随便删除
> select * from stock //查一下,发现600015记录还在
name: stock
-----------
time            code    name    price
1564559969067570478 601668  "中国建筑"  5.89
1564560004602845932 600015  "华夏银行"  7.56

//下面使用INSERT再覆盖一遍, 显式指定time. 充当update的功能
> INSERT stock,code=600015,name="华夏银行" price=7.57 1564560004602845932
> select * from stock
name: stock
-----------
time            code    name    price
1564559969067570478 601668  "中国建筑"  5.89
1564560004602845932 600015  "华夏银行"  7.57

国货当自强,TDEngine开源
国人公司写的。专为物联网而生的。
https://www.taosdata.com/cn/

8) ORM 数据库框架

GreenDao Android上比较有名的

比较好的教程看这里
https://www.jianshu.com/p/53083f782ea2

用法,在gradle里引入相应的库,定义POJO, 并使用注解,
在Application中初始化

    private void initGreenDao(){
        DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "mygreen.db");//看,这里定义了数据库的名字
        SQLiteDatabase db = helper.getWritableDatabase();
        DaoMaster daoMaster = new DaoMaster(db);
        daoSession = daoMaster.newSession();
    }

使用DaoSession来操作,以下是insert的一个例子:

        DaoSession daoSession = App.getInstance().getDaoSession();
        //StudentDao studentDao = daoSession.getStudentDao();
        Student student = new Student();
        student.setName("Stephen");
        student.setAge(19);
        student.setStudentNo("8899");
        daoSession.insert(student);
        //studentDao.insert(student);

LitePal 是国产的,运行在android上
源码地址https://github.com/LitePalFramework/LitePal

使用方式:
app build.gradle

    implementation 'org.litepal.android:java:3.0.0'

自定义Application

public class MyApplication extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
        LitePal.initialize(this);//这是全局的初始化
    }
}

需要创建assets/litepal.xml,用来描述数据库名字(看上去只能支持1个数据库),数据库版本,bean的描述

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <!--
        Define the database name of your application.
        By default each database name should be end with .db.
        If you didn't name your database end with .db,
        LitePal would plus the suffix automatically for you.
        For example:
        <dbname value="demo" />
    -->
    <dbname value="demo" />

    <!--
        Define the version of your database. Each time you want
        to upgrade your database, the version tag would helps.
        Modify the models you defined in the mapping tag, and just
        make the version value plus one, the upgrade of database
        will be processed automatically without concern.
            For example:
        <version value="1" />
    -->
    <version value="1" />

    <!--
        Define your models in the list with mapping tag, LitePal will
        create tables for each mapping class. The supported fields
        defined in models will be mapped into columns.
        For example:
        <list>
            <mapping class="com.test.model.Reader" />
            <mapping class="com.test.model.Magazine" />
        </list>
    -->
    <list>

        <mapping class="com.zhuguangsheng.mylitesqlitedemo.beans.Album" />
        <mapping class="com.zhuguangsheng.mylitesqlitedemo.beans.Song" />

    </list>

    <!--
        Define where the .db file should be. "internal" means the .db file
        will be stored in the database folder of internal storage which no
        one can access. "external" means the .db file will be stored in the
        path to the directory on the primary external storage device where
        the application can place persistent files it owns which everyone
        can access. "internal" will act as default.
        For example:
        <storage value="external" />
    -->

</litepal>

定义我们的bean,这里比较特别的是,需要继承LitePalSupport

public class Song extends LitePalSupport {

    @Column(nullable = false)
    private String name;

    private int duration;

    @Column(ignore = true)
    private String uselessField;

    private Album album;

    // generated getters and setters.
   //getter & setter省略
   
}

插入一条数据实验

        Song song1 = new Song();
        song1.setName("song1");
        song1.setDuration(320);
        song1.setAlbum(album);
        song1.save();

mybatis 是java后台软件常用的

未完待续

mybatis plus是国产在mybatis之上的改进

未完待续

国货当自强!

8) 网络存储文件

这里的文件,一般是指比较大的,不宜通过http(s) REST接口传输的,例如,大于1MB了。

ftp 上传和下载

在局域网中,特别是toB项目只运行在一个局域网中,有时候为了简单,往往使用ftp做为文件服务器。
ftp的服务器搭建比较简单,windows上有IIS, linux vsftpd

客户端连接软件可以使用Filezilla辅助测试


image.png

ftp需要的参数的主机地址,端口,用户名,密码

编程时,以android为例,使用commons-net-2.2.jar 库
需要注意的问题是:由于用户名和密码都要存储在客户端,记得要在本地加密。别写在Java里易被破解,不防用C语言封装一下防静态破解。

OSS与CDN

OSS这里引用的是阿里云的概念
在腾讯云上OSS叫COS云对象存储 (有没有联想到COSPlay...)
OSS: Object Save Service 对象存储服务
OSS里面有个重要概念,叫bucket,翻译过来是一个桶。
我们的对象Object其实就是一个物品, 我们把物品放到桶里。
一个账户下可以定义N个桶。一个桶里又可以放M个对象。

而CDN ,即内容分发网络。是用来下载对象的。
为什么不直接从OSS下载呢? 因为OSS下载流量贵。
而CDN会在全世界创建更多节点,下载更就近,更快。

OSS到CDN有一个刷新过程,或者说“分发”过程。它是需要一些条件触发的。所以,如果遇到你在CDN下载的文件是个旧的,那需要后台运维人员去强制刷新一下OSS到CDN

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容