1. 序言
从前说到后,数据存储那点事。
在计算机及嵌入式软件中,不可避免的要实现数据存储。
本文从硬件层到软件层,从前台到后台梳理了数据存储的方式和内容。并且会不断更新。欢迎Mark。
目的是在工作中,了解不同数据存储方式的特点,以选择合适的存储方式。以及存储策略。
如果使用英文access, 翻译成中文是存取,即有存又有取。在本文中不区分“存取”和“存储”,认为是同一含义。
另外着重关注java, android等在存储方面的细节。
然而,前后端技术是相通的。做单片机和Android的同学,了解一下后端的知识体系也是OK的,反之也是,不是吗?
2. 计算机基本组成
无论是计算机(PC, Server)还是嵌入式软件,目前的整体架构大多都是冯诺依曼的。
1.运算器
2.控制器
3.存储器
4.输入设备
5.输出设备
其中,存储器分内部存储和外部存储
内部存储:又称内存,内存条,内存颗粒(IC)
外部存储:又称外存,硬盘,闪存。
本篇讨论的主要是外部存储
3. 外部存储主要的硬件
EEPROM
电可擦除ROM
一般在单片机系统中使用到。
我们知道ROM是Read Only Memory,只能读不能写。但EEPROM是电可擦除的。就是说能读能写。
一般用来存储少量数据.
单片机和EEPROM芯片的通信,一般是用SPI或I2C总线标准。比如这是一款24C02芯片
当然,其实现在很多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芯片照片:
eMMC, UFS, 固态硬盘
其实都是Flash的变种,是封装了硬件协议和磨损均衡之后的成品。更容易接入我们的嵌入式CPU或者电脑系统
eMMC图片:
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) 对象数据库
对象数据库的例子: 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)是一个为了用于处理时间序列数据而优化的软件系统,其按时间数值或时间范围进行索引。
写操作的频率远远大于读,并且存在一定的时间窗口。此外,时序数据库通常极少更新数据,存在一定的覆盖写,并且支持批量删除操作。
时序数据库经常用于以下类似场景:
- 物联网传感器探测值,例如温度
- 健康设备数据值
- 网站PV/UV
- 股票行情
- 监控系统(
《 互联网级监控系统必备-时序数据库之Influxdb技术
》https://www.cnblogs.com/tianqing/p/7152940.html
《饿了么 Influxdb 实践之路》介绍
万台机器的监控方案https://blog.51cto.com/welcomeweb/2136300?source=dra
来看排名前10位的TSDB榜单:
选型哪个好呢? 网上讨论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辅助测试
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