PyTorch使用LMDB数据库加速文件读取

文章目录

  • PyTorch使用LMDB数据库加速文件读取
    • 背景介绍
    • 具体操作
      • LMDB主要类
        • `lmdb.Environment`
        • `lmdb.Transaction`
        • `Imdb.Cursor`
      • 操作流程
      • 创建图像数据集
      • 配合DataLoader
    • 参考链接

原始文档:https://www.yuque.com/lart/ugkv9f/hbnym1

对于数据库的了解较少,文章中大部分的介绍主要来自于各种博客和LMDB的文档,但是文档中的介绍,默认是已经了解了数据库的许多知识,这导致目前只能囫囵吞枣,待之后仔细了解后再重新补充内容。

背景介绍

文章https://blog.csdn.net/jyl1999xxxx/article/details/53942824中介绍了使用LMDB的原因:

Caffe使用LMDB来存放训练/测试用的数据集,以及使用网络提取出的feature(为了方便,以下还是统称数据集)。数据集的结构很简单,就是大量的矩阵/向量数据平铺开来。数据之间没有什么关联,数据内没有复杂的对象结构,就是向量和矩阵。既然数据并不复杂,Caffe就选择了LMDB这个简单的数据库来存放数据。

LMDB的全称是Lightning Memory-Mapped Database,闪电般的内存映射数据库。它文件结构简单,一个文件夹,里面一个数据文件,一个锁文件。数据随意复制,随意传输。它的访问简单,不需要运行单独的数据库管理进程,只要在访问数据的代码里引用LMDB库,访问时给文件路径即可。

图像数据集归根究底从图像文件而来。引入数据库存放数据集,是为了减少IO开销。读取大量小文件的开销是非常大的,尤其是在机械硬盘上。LMDB的整个数据库放在一个文件里,避免了文件系统寻址的开销。LMDB使用内存映射的方式访问文件,使得文件内寻址的开销非常小,使用指针运算就能实现。数据库单文件还能减少数据集复制/传输过程的开销。一个几万,几十万文件的数据集,不管是直接复制,还是打包再解包,过程都无比漫长而痛苦。LMDB数据库只有一个文件,你的介质有多块,就能复制多快,不会因为文件多而慢如蜗牛。

在文章http://shuokay.com/2018/05/14/python-lmdb/中类似提到:

为什么要把图像数据转换成大的二进制文件?
简单来说,是因为读写小文件的速度太慢。那么,不禁要问,图像数据也是二进制文件,单个大的二进制文件例如 LMDB 文件也是二进制文件,为什么单个图像读写速度就慢了呢?这里分两种情况解释。

  1. 机械硬盘的情况:机械硬盘的每次读写启动时间比较长,例如磁头的寻道时间占比很高,因此,如果单个小文件读写,尤其是随机读写单个小文件的时候,这个寻道时间占比就会很高,最后导致大量读写小文件的时候时间会很浪费;
  2. NFS 的情况:在 NFS 的场景下,系统的一次读写首先要进行上百次的网络通讯,并且这个通讯次数和文件的大小无关。因此,如果是读写小文件,这个网络通讯时间占据了整个读写时间的大部分。

固态硬盘的情况下应该也会有一些类似的开销,目前没有研究过。

总而言之,使用LMDB可以为我们的数据读取进行加速。

具体操作

LMDB主要类

pip install lmdb

lmdb.Environment

lmdb.open() 这个方法实际上是 class lmdb.Environment(path, map_size=10485760, subdir=True, readonly=False, metasync=True, sync=True, map_async=False, mode=493, create=True, readahead=True, writemap=False, meminit=True, max_readers=126, max_dbs=0, max_spare_txns=1, lock=True) 的一个别名(shortcut),二者是等价的。关于这个类:https://lmdb.readthedocs.io/en/release/#environment-class

这是数据库环境的结构。 一个环境可能包含多个数据库,所有数据库都驻留在同一共享内存映射和基础磁盘文件中。要写入环境,必须创建事务(Transaction)。 允许同时进行一次写入事务,但是即使存在写入事务,读取事务的数量也没有限制。

几个重要的实例方法:

  • begin(db=None, parent=None, write=False, buffers=False): 可以调用事务类 lmdb.Transaction
  • open_db(key=None, txn=None, reverse_key=False, dupsort=False, create=True, integerkey=False, integerdup=False, dupfixed=False): 打开一个数据库,返回一个不透明的句柄。重复Environment.open_db() 调用相同的名称将返回相同的句柄。作为一个特殊情况,主数据库总是开放的。命名数据库是通过在主数据库中存储一个特殊的描述符来实现的。环境中的所有数据库共享相同的文件。因为描述符存在于主数据库中,所以如果已经存在与数据库名称匹配的 key ,创建命名数据库的尝试将失败。此外,查找和枚举可以看到key 。如果主数据库keyspace与命名数据库使用的名称冲突,则将主数据库的内容移动到另一个命名数据库。
>>> env = lmdb.open('/tmp/test', max_dbs=2)
>>> with env.begin(write=True) as txn
...     txn.put('somename', 'somedata')
>>> # Error: database cannot share name of existing key!
>>> subdb = env.open_db('somename')

lmdb.Transaction

这和事务对象有关。

class lmdb.Transaction(env, db=None, parent=None, write=False, buffers=False)

关于这个类的参数:https://lmdb.readthedocs.io/en/release/#transaction-class

所有操作都需要事务句柄,事务可以是只读或读写的。写事务可能不会跨越线程。事务对象实现了上下文管理器协议,因此即使面对未处理的异常,也可以可靠地释放事务:

# Transaction aborts correctly:
with env.begin(write=True) as txn:crash()
# Transaction commits automatically:
with env.begin(write=True) as txn:txn.put('a', 'b')

这个类的实例包含着很多有用的操作方法。

  • abort(): 中止挂起的事务。重复调用 abort() 在之前成功的 commit()abort() 后或者在相关环境关闭后是没有效果的。
  • commit(): 提交挂起的事务。
  • cursor(db=None): Shortcut for lmdb.Cursor(db, self)
  • delete(key, value=’’, db=None): Delete a key from the database.
    • key: The key to delete.
    • value:如果数据库是以 dupsort = True 打开的,并且 value 不是空的 bytestring ,则删除仅与此 (key, value) 对匹配的元素,否则该 key 的所有值都将被删除。
    • Returns True if at least one key was deleted.
  • drop(db, delete=True): 删除命名数据库中的所有键,并可选地删除命名数据库本身。删除命名数据库会导致其不可用,并使现有cursors无效。
  • get(key, default=None, db=None): 获取匹配键的第一个值,如果键不存在,则返回默认值。cursor必须用于获取 dupsort = True 数据库中的 key 的所有值。
  • id(): 返回事务的ID。这将返回与此事务相关联的标识符。对于只读事务,这对应于正在读取的快照; 并发读取器通常具有相同的事务ID。
  • pop(key, db=None): 使用临时cursor调用 Cursor.pop()
    • db: 要操作的命名数据库。如果未指定,默认为事务构造函数被给定的数据库。
  • put(key, value, dupdata=True, overwrite=True, append=False, db=None): 存储一条记录(record),如果记录被写入,则返回 True ,否则返回 False ,以指示key已经存在并且 overwrite = False 。成功后,cursor位于新记录上。
    • key: Bytestring key to store.
    • value: Bytestring value to store.
    • dupdata: 如果 True ,并且数据库是用 dupsort = True 打开的,如果给定 key 已经存在,则添加键值对作为副本。否则覆盖任何现有匹配的 key
    • overwrite: If False , do not overwrite any existing matching key.
    • append: 如果为 True ,则将对附加到数据库末尾,而不首先比较其顺序。附加不大于现有最高 keykey 将导致损坏。
    • db: 要操作的命名数据库。如果未指定,默认为事务构造函数被给定的数据库。
  • replace(key, value, db=None): 使用临时cursor调用 Cursor.replace() .
  • db: Named database to operate on. If unspecified, defaults to the database given to the Transaction constructor.
  • stat(db): Return statistics like Environment.stat() , except for a single DBI. db must be a database handle returned by open_db() .

Imdb.Cursor

class lmdb.Cursor(db, txn) 是用于在数据库中导航(navigate)的结构。

  • db: Database to navigate.
  • txn: Transaction to navigate.

As a convenience, Transaction.cursor() can be used to quickly return a cursor:

>>> env = lmdb.open('/tmp/foo')
>>> child_db = env.open_db('child_db')
>>> with env.begin() as txn:
...     cursor = txn.cursor()           # Cursor on main database.
...     cursor2 = txn.cursor(child_db)  # Cursor on child database.

游标以未定位的状态开始。如果在这种状态下使用 iternext()iterprev() ,那么迭代将分别从开始处和结束处开始。迭代器直接使用游标定位,这意味着在同一游标上存在多个迭代器时会产生奇怪的行为

从Python绑定的角度来看,一旦任何扫描或查找方法(例如 next()prev_nodup()set_range() )返回 False 或引发异常,游标将返回未定位状态。这主要是为了确保在面对任何错误条件时语义的安全性和一致性。
当游标返回到未定位的状态时,它的 key()value() 返回空字符串,表示没有活动的位置,尽管在内部,LMDB游标可能仍然有一个有效的位置。
这可能会导致在迭代 dupsort=True 数据库的 key 时出现一些令人吃惊的行为,因为 iternext_dup() 等方法将导致游标显示为未定位,尽管它返回 False 只是为了表明当前键没有更多的值。在这种情况下,简单地调用 next() 将导致在下一个可用键处继续迭代。
This behaviour may change in future.

Iterator methods such as iternext() and iterprev() accept keys and values arguments. If both are True , then the value of item() is yielded on each iteration. If only keys is True , key() is yielded, otherwise only value() is yielded.

在迭代之前,游标可能定位在数据库中的任何位置

>>> with env.begin() as txn:
...     cursor = txn.cursor()
...     if not cursor.set_range('5'): # Position at first key >= '5'.
...         print('Not found!')
...     else:
...         for key, value in cursor: # Iterate from first key >= '5'.
...             print((key, value))

不需要迭代来导航,有时会导致丑陋或低效的代码。在迭代顺序不明显的情况下,或者与正在读取的数据相关的情况下,使用 set_key()set_range()key()value()item() 可能是更好的选择。

>>> # Record the path from a child to the root of a tree.
>>> path = ['child14123']
>>> while path[-1] != 'root':
...     assert cursor.set_key(path[-1]), \
...         'Tree is broken! Path: %s' % (path,)
...     path.append(cursor.value())

几个实例方法:

  • set_key(key): Seek exactly to key, returning True on success or False if the exact key was not found. 对于 set_key() ,空字节串是错误的。对于使用 dupsort=True 打开的数据库,移动到键的第一个值(复制)。
  • set_range(key): Seek to the first key greater than or equal to key , returning True on success, or False to indicate key was past end of database. Behaves like first() if key is the empty bytestring. 对于使用 dupsort=True 打开的数据库,移动到键的第一个值(复制)。
  • get(key, default=None): Equivalent to set_key() , except value() is returned when key is found, otherwise default.
  • item(): Return the current (key, value) pair.
  • key(): Return the current key.
  • value(): Return the current value.

操作流程

概况地讲,操作LMDB的流程是:

  • 通过 env = lmdb.open() 打开环境
  • 通过 txn = env.begin() 建立事务
  • 通过 txn.put(key, value) 进行插入和修改
  • 通过 txn.delete(key) 进行删除
  • 通过 txn.get(key) 进行查询
  • 通过 txn.cursor() 进行遍历
  • 通过 txn.commit() 提交更改

这里要注意:

  1. putdelete 后一定注意要 commit ,不然根本没有存进去
  2. 每一次 commit 后,需要再定义一次 txn=env.begin(write=True)

来自https://github.com/kophy/py4db的代码:

#!/usr/bin/env pythonimport lmdb
import os, sysdef initialize():env = lmdb.open("students");return env;def insert(env, sid, name):txn = env.begin(write = True);txn.put(str(sid), name);txn.commit();def delete(env, sid):txn = env.begin(write = True);txn.delete(str(sid));txn.commit();def update(env, sid, name):txn = env.begin(write = True);txn.put(str(sid), name);txn.commit();def search(env, sid):txn = env.begin();name = txn.get(str(sid));return name;def display(env):txn = env.begin();cur = txn.cursor();for key, value in cur:print (key, value);env = initialize();print "Insert 3 records."
insert(env, 1, "Alice");
insert(env, 2, "Bob");
insert(env, 3, "Peter");
display(env);print "Delete the record where sid = 1."
delete(env, 1);
display(env);print "Update the record where sid = 3."
update(env, 3, "Mark");
display(env);print "Get the name of student whose sid = 3."
name = search(env, 3);
print name;env.close();os.system("rm -r students");

创建图像数据集

这里主要借鉴自https://github.com/open-mmlab/mmsr/blob/master/codes/data_scripts/create_lmdb.py的代码。

改写为:

import glob
import os
import pickle
import sysimport cv2
import lmdb
import numpy as np
from tqdm import tqdmdef main(mode):proj_root = '/home/lart/coding/TIFNet'datasets_root = '/home/lart/Datasets/'lmdb_path = os.path.join(proj_root, 'datasets/ECSSD.lmdb')data_path = os.path.join(datasets_root, 'RGBSaliency', 'ECSSD/Image')if mode == 'creating':opt = {'name': 'TrainSet','img_folder': data_path,'lmdb_save_path': lmdb_path,'commit_interval': 100,  # After commit_interval images, lmdb commits'num_workers': 8,}general_image_folder(opt)elif mode == 'testing':test_lmdb(lmdb_path, index=1)def general_image_folder(opt):"""Create lmdb for general image foldersIf all the images have the same resolution, it will only store one copy of resolution info.Otherwise, it will store every resolution info."""img_folder = opt['img_folder']lmdb_save_path = opt['lmdb_save_path']meta_info = {'name': opt['name']}if not lmdb_save_path.endswith('.lmdb'):raise ValueError("lmdb_save_path must end with 'lmdb'.")if os.path.exists(lmdb_save_path):print('Folder [{:s}] already exists. Exit...'.format(lmdb_save_path))sys.exit(1)# read all the image paths to a listprint('Reading image path list ...')all_img_list = sorted(glob.glob(os.path.join(img_folder, '*')))# cache the filename, 这里的文件名必须是ascii字符keys = []for img_path in all_img_list:keys.append(os.path.basename(img_path))# create lmdb environment# 估算大概的映射空间大小data_size_per_img = cv2.imread(all_img_list[0], cv2.IMREAD_UNCHANGED).nbytesprint('data size per image is: ', data_size_per_img)data_size = data_size_per_img * len(all_img_list)env = lmdb.open(lmdb_save_path, map_size=data_size * 10)# map_size:# Maximum size database may grow to; used to size the memory mapping. If database grows larger# than map_size, an exception will be raised and the user must close and reopen Environment.# write data to lmdbtxn = env.begin(write=True)resolutions = []tqdm_iter = tqdm(enumerate(zip(all_img_list, keys)), total=len(all_img_list), leave=False)for idx, (path, key) in tqdm_iter:tqdm_iter.set_description('Write {}'.format(key))key_byte = key.encode('ascii')data = cv2.imread(path, cv2.IMREAD_UNCHANGED)if data.ndim == 2:H, W = data.shapeC = 1else:H, W, C = data.shaperesolutions.append('{:d}_{:d}_{:d}'.format(C, H, W))txn.put(key_byte, data)if (idx + 1) % opt['commit_interval'] == 0:txn.commit()# commit 之后需要再次 begintxn = env.begin(write=True)txn.commit()env.close()print('Finish writing lmdb.')# create meta information# check whether all the images are the same sizeassert len(keys) == len(resolutions)if len(set(resolutions)) <= 1:meta_info['resolution'] = [resolutions[0]]meta_info['keys'] = keysprint('All images have the same resolution. Simplify the meta info.')else:meta_info['resolution'] = resolutionsmeta_info['keys'] = keysprint('Not all images have the same resolution. Save meta info for each image.')pickle.dump(meta_info, open(os.path.join(lmdb_save_path, 'meta_info.pkl'), "wb"))print('Finish creating lmdb meta info.')def test_lmdb(dataroot, index=1):env = lmdb.open(dataroot, readonly=True, lock=False, readahead=False, meminit=False)meta_info = pickle.load(open(os.path.join(dataroot, 'meta_info.pkl'), "rb"))print('Name: ', meta_info['name'])print('Resolution: ', meta_info['resolution'])print('# keys: ', len(meta_info['keys']))# read one imagekey = meta_info['keys'][index]print('Reading {} for test.'.format(key))with env.begin(write=False) as txn:buf = txn.get(key.encode('ascii'))img_flat = np.frombuffer(buf, dtype=np.uint8)C, H, W = [int(s) for s in meta_info['resolution'][index].split('_')]img = img_flat.reshape(H, W, C)cv2.namedWindow('Test')cv2.imshow('Test', img)cv2.waitKeyEx()if __name__ == "__main__":# mode = creating or testingmain(mode='creating')

配合DataLoader

这里仅对训练集进行LMDB处理,测试机依旧使用的原始的读取图片的方式。

import os
import pickleimport lmdb
import numpy as np
from PIL import Image
from prefetch_generator import BackgroundGenerator
from torch.utils.data import DataLoader, Dataset
from torchvision import transformsfrom utils import joint_transformsdef _get_paths_from_lmdb(dataroot):"""get image path list from lmdb meta info"""meta_info = pickle.load(open(os.path.join(dataroot, 'meta_info.pkl'),'rb'))paths = meta_info['keys']sizes = meta_info['resolution']if len(sizes) == 1:sizes = sizes * len(paths)return paths, sizesdef _read_img_lmdb(env, key, size):"""read image from lmdb with key (w/ and w/o fixed size)size: (C, H, W) tuple"""with env.begin(write=False) as txn:buf = txn.get(key.encode('ascii'))img_flat = np.frombuffer(buf, dtype=np.uint8)C, H, W = sizeimg = img_flat.reshape(H, W, C)return imgdef _make_dataset(root, prefix=('.jpg', '.png')):img_path = os.path.join(root, 'Image')gt_path = os.path.join(root, 'Mask')img_list = [os.path.splitext(f)[0] for f in os.listdir(gt_path)if f.endswith(prefix[1])]return [(os.path.join(img_path, img_name + prefix[0]),os.path.join(gt_path, img_name + prefix[1]))for img_name in img_list]class TestImageFolder(Dataset):def __init__(self, root, in_size, prefix):self.imgs = _make_dataset(root, prefix=prefix)self.test_img_trainsform = transforms.Compose([# 输入的如果是一个tuple,则按照数据缩放,但是如果是一个数字,则按比例缩放到短边等于该值transforms.Resize((in_size, in_size)),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])def __getitem__(self, index):img_path, gt_path = self.imgs[index]img = Image.open(img_path).convert('RGB')img_name = (img_path.split(os.sep)[-1]).split('.')[0]img = self.test_img_trainsform(img)return img, img_namedef __len__(self):return len(self.imgs)class TrainImageFolder(Dataset):def __init__(self, root, in_size, scale=1.5, use_bigt=False):self.use_bigt = use_bigtself.in_size = in_sizeself.root = rootself.train_joint_transform = joint_transforms.Compose([joint_transforms.JointResize(in_size),joint_transforms.RandomHorizontallyFlip(),joint_transforms.RandomRotate(10)])self.train_img_transform = transforms.Compose([transforms.ColorJitter(0.1, 0.1, 0.1),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225])  # 处理的是Tensor])# ToTensor 操作会将 PIL.Image 或形状为 H×W×D,数值范围为 [0, 255] 的 np.ndarray 转换为形状为 D×H×W,# 数值范围为 [0.0, 1.0] 的 torch.Tensor。self.train_target_transform = transforms.ToTensor()self.gt_root = '/home/lart/coding/TIFNet/datasets/DUTSTR/DUTSTR_GT.lmdb'self.img_root = '/home/lart/coding/TIFNet/datasets/DUTSTR/DUTSTR_IMG.lmdb'self.paths_gt, self.sizes_gt = _get_paths_from_lmdb(self.gt_root)self.paths_img, self.sizes_img = _get_paths_from_lmdb(self.img_root)self.gt_env = lmdb.open(self.gt_root, readonly=True, lock=False, readahead=False,meminit=False)self.img_env = lmdb.open(self.img_root, readonly=True, lock=False, readahead=False,meminit=False)def __getitem__(self, index):gt_path = self.paths_gt[index]img_path = self.paths_img[index]gt_resolution = [int(s) for s in self.sizes_gt[index].split('_')]img_resolution = [int(s) for s in self.sizes_img[index].split('_')]img_gt = _read_img_lmdb(self.gt_env, gt_path, gt_resolution)img_img = _read_img_lmdb(self.img_env, img_path, img_resolution)if img_img.shape[-1] != 3:img_img = np.repeat(img_img, repeats=3, axis=-1)img_img = img_img[:, :, [2, 1, 0]]  # bgr => rgbimg_gt = np.squeeze(img_gt, axis=2)gt = Image.fromarray(img_gt, mode='L')img = Image.fromarray(img_img, mode='RGB')img, gt = self.train_joint_transform(img, gt)gt = self.train_target_transform(gt)img = self.train_img_transform(img)if self.use_bigt:gt = gt.ge(0.5).float()  # 二值化img_name = self.paths_img[index]return img, gt, img_namedef __len__(self):return len(self.paths_img)class DataLoaderX(DataLoader):def __iter__(self):return BackgroundGenerator(super(DataLoaderX, self).__iter__())

参考链接

  • 文档:https://lmdb.readthedocs.io/en/release/
  • http://shuokay.com/2018/05/14/python-lmdb/
  • 关于LMDB的介绍:https://blog.csdn.net/jyl1999xxxx/article/details/53942824
  • 代码示例:
    • https://github.com/kophy/py4db
    • https://www.jianshu.com/p/66496c8726a1
  • https://blog.csdn.net/ayst123/article/details/44077903
  • https://github.com/open-mmlab/mmsr
  • torchvision中涉及到lmdb使用的一部分代码:https://github.com/pytorch/vision/blob/master/torchvision/datasets/lsun.py

PyTorch使用LMDB数据库加速文件读取相关推荐

  1. linux内核态加速文件读取,学习在kernel态下使用NEON对算法进行加速的方法

    本文跟着小编一起来学习在linux kernel态下如何使用NEON对算法进行加速的技巧,内容通过图文实例给大家做了详细分析,一起来看下. ARM处理器从cortex系列开始集成NEON处理单元,该单 ...

  2. c#大文件读取和写入数据库

    c#大文件读取和写入数据库(带进度条的源代码) 最近一个项目需要将大文件写入和读取到数据库,觉得可能很多人也需要相关得东西,所以就将代码帖出来 protected int state = 0; //表 ...

  3. java struts2 excel上传_文件上传方法,使用Struts2,实现Excel文件读取并写入数据库技术...

    文件上传方法,使用Struts2,实现Excel文件读取并写入数据库技术 如题:文件信息的批量导入-- 项目中经常会遇到客户的一些单表信息的数据批量导入,也就是提供定制Excel表,再把Excel表中 ...

  4. ctfshow-WEB-web14( 利用数据库读写功能读取网站敏感文件)

    ctf.show WEB模块第14关是一个SQL注入漏洞, 绕过switch循环后可以拿到一个登录界面, 登录界面存在SQL注入, 脱库以后会提示flag在另一个文件中, 利用数据库的文件读写功能读取 ...

  5. fn_dblog_如何使用fn_dblog和fn_dump_dblog直接在SQL Server数据库中连续读取事务日志文件数据

    fn_dblog 大纲 (Outline) In this article, we'll discuss how to read SQL Server transaction logs. This a ...

  6. 简单开发的android阅读器源码,包含了读取数据库和文件流处理功能

    原文:简单开发的android阅读器源码,包含了读取数据库和文件流处理功能 源代码下载地址:http://www.zuidaima.com/share/1838906559466496.htm 简单地 ...

  7. android 读取本地数据库db文件(Android sqlite)

    我们知道Android中有四种数据存储方式: SharedPreference存储 content provider SQLite数据库存储 文件存储 今天我们主要说 本地数据库sqlite这种方式, ...

  8. pytorch构造IterableDataset,流式读取文件夹,文件夹下所有大数据文件,逐个文件!逐行读取!(pytorch Data学习四)

    我有个文件夹,里面有一万个文件,每个文件都是N个T的容量,那么这就需要逐个文件.逐行读取,读取方法如下: 核心:构造IterableDataset IterableDataset需要设置两个东西,一个 ...

  9. 『全闪实测』数据库加速解决方案

    方案背景 背景概述 随着互联网和电子商务的迅猛发展,传统的大型结构化数据库系统在企业应用中承载着越来越多的应用,重载情况越来越突出,担当着企业业务和信息系统核心的重任. 但是,本来是为了传统业务的开展 ...

  10. CentOS下postgres怎么恢复数据库.bak文件_数据架构选型必读:4月数据库产品技术解析...

    本期要点 DB-Engines数据库排行榜 一.RDBMS MySQL发布8.0.20版本,5.6版本于2021年2月停止更新 DB2发布11.5.2版本,且看容器化是否可为DB2注入新活力 Post ...

最新文章

  1. 饿了么监控体系:从架构的减法中演进而来
  2. 03_属性别名映射的配置
  3. php 生成小程序二维码
  4. POI的入门:创建单元格设置数据
  5. 《计算机算法设计与分析》题目汇总
  6. Base64编码简介及在java中的使用
  7. 观察者模式java类图_初探Java设计模式------观察者模式
  8. 3.0 Android组件之间的信使Intent
  9. Hadoop公司考试题(基础)
  10. iOS 获取设备的方向
  11. Struts2_01_开发过程与实例说明
  12. FreeMarker模板引擎实现页面静态化
  13. linux怎么用中文显示,linux中文显示设置
  14. 今天睡眠质量记录88分
  15. OneFlow源码解析:静态图与运行时
  16. 金字塔结构式表达利器
  17. 马克飞象自定义代码段风格
  18. 增量式编码器和绝对式编码器区别
  19. preLaunchTask“C/C++:g++.exe生成活动文件“已终止,退出代码为-1
  20. python迷宫小游戏代码_python迷宫游戏,迷宫生成,解决与可视化

热门文章

  1. 10款视频转码软件的H264低码率高画质转码评测
  2. 计算机病毒及解决方法,3种电脑病毒及解决方法
  3. Advanced IP Scanner - 网络扫描器
  4. 怎么样使prestashop 运行速度更快
  5. android otg dac,随身HiFi 安卓OTG功能在音频上的妙用
  6. SSIS(简单数据抽取过程介绍)
  7. 量子力学计算机原理,量子力学的基本原理
  8. VARCHART XGantt与活动互动教程指南
  9. acdsee免费版跳过注册账户_加快Win 10启动速度,直接跳过锁屏登录界面
  10. 利用图神经网络进行社交机器人检测