Sqlite可以处理多少?多个线程同时插入到sqlite中

1136 篇文章 56 订阅
228 篇文章 13 订阅

目录

介绍

对你有什么好处

背景

让我们一起构建控制台应用程序

发现.NET版本

创建新项目(使用顶级语句)

运行快速测试

通过Nuget添加实体框架

添加新类来表示数据

使用初始化构造新的ThreadData对象

Entity Framework上下文类

更少的安装:未使用dotnet-ef工具

检查Sqlite DB文件是否存在

添加一些代码到Program.cs并尝试一下

你有sqlite,不是吗?

向ThreadData数据库添加数据

让我们的应用程序在不同的线程上插入数据

用于多个线程的WriteData函数

运行代码

编辑:使用命令行设置插入计数

默认值

即使有错误,也不会丢失数据

添加了一些基本的时序数据

用于检查数据的有用查询

结论


您也可以在我的 GitHub存储库[^] 中获取代码。

介绍

我有一个网站,它使用一个简单的Sqlite数据库作为后端数据。

也许这听起来很奇怪,但这是官方Sqlite文档对此的看法:

何时使用[^]

网站SQLite非常适合作为大多数中低流量网站(即大多数网站)的数据库引擎。SQLite可以处理的网络流量取决于网站使用其数据库的程度。一般来说,任何点击量低于100K/天的网站都应该可以正常使用SQLite。每天100K次点击的数字是一个保守的估计,而不是一个硬性的上限。SQLite已被证明可以处理10倍的流量。

当然,SQLite网站(https://www.sqlite.org/)使用SQLite本身,截至撰写本文时(2015年),它每天处理大约400K500K HTTP请求,其中大约15-20%是接触数据库的动态页面。动态内容每个网页使用大约200SQL语句。

如果您从未对Sqlite进行过太多研究(即使您研究过),这些陈述可能会让您感到震惊,因为大多数人认为Sqlite是移动设备的数据库。

对你有什么好处

我希望这篇文章能让你快速了解Sqlite对你自己的项目是否可行。以下是我将提供的内容:

  1. 教程/演练,演示如何生成快速的.NET Core控制台应用。
  2. 有关使用“dotnet”命令行的一些有用说明(各种命令将帮助你学习构建和支持使用dotnet构建的应用。
  3. 您可以根据自己的目的更改小型控制台应用程序
  4. 通过生成工作线程(将同时插入Sqlite的工作线程)对C#线程进行简单介绍
  5. 可以检查以发现/决定是否可以信任/使用Sqlite的数据。

背景

但是,我仍然很好奇大量并发请求会发生什么。

如果在另一个用户尝试从数据库中读取数据库时,数据库中有大量并发插入,会发生什么情况?

这就是本文(轻微)调查的内容。我说得很轻,因为我们将创建一个完整的控制台程序来对本地Sqlite数据库运行一些并发插入,但我真的很想听听有关结果含义的输入。我将提供一种查看一些有趣数据的方法,希望我能听到对结果含义有一些意见的人的反馈。

让我们开始吧。

让我们一起构建控制台应用程序

我将Visual Studio Code.NET Core 8.0.202SDK)和.NET运行时8.0.3的安装配合使用。

发现.NET版本

如果已安装.NET Core,并且想要查看您拥有的版本,则只需打开终端窗口并运行:

$ dotnet --list-sdks

$ dotnet --list-runtimes

您将看到一些输出,这些输出将使您了解正在运行的内容。

这是我的样子:

创建新项目(使用顶级语句)

由于这是一个简单的小应用程序,我们将使用基本的控制台应用程序和顶级语句[^]来创建它。

我只在我的家庭桌面上运行Ubuntu 22.04.3 LTS,我还运行Mac ProM3)。

我只在VirtualBox中远程(用于工作)和本地运行Windows,但以下命令将允许您在这三个平台中的任何一个平台上创建控制台应用程序。

打开终端窗口,然后转到要在其中创建项目的文件夹。

将创建一个单独的项目文件夹以包含项目中的所有文件。

$ dotnet new console -o sqliteThreads

现在,您将拥有一个名为 sqliteThreads 的新文件夹,其中包含基本控制台应用。

运行快速测试

可以快速运行该程序,以确保正确设置了.NET Core安装。

首先,将目录更改为新的项目文件夹:

$ cd sqliteThreads
$ dotnet run

第二个命令将构建应用程序并运行它,您应该看到基本的Hello, World!打印到控制台窗口。

通过Nuget添加实体框架

我决定使用Entity Framework来加快所有这些操作,因此这是我们要添加到项目中的第一件事。

转到终端中的项目文件夹并运行以下命令:

$ dotnet add package Microsoft.EntityFrameworkCore.Sqlite 

显然,这增加了允许我们将Entity FrameworkSqlite一起使用的库。

如果您对使用实体框架和Sqlite创建控制台应用程序的其他详细信息感兴趣,可以查看我引用的Microsoft文章以学习执行此操作[^]

基本上,在运行命令添加该包后,将下载所有必要的包,并且.csproj文件将添加以下内容(以引用EF sqlite包):

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />

添加nuget引用后,我总是进行构建,以确保一切仍然正常工作。

$ dotnet build

添加新类来表示数据

让我们添加一个新类,我们将用它来表示我们将写入表的数据。

我们需要一些属性来帮助我们在写入数据后检查数据。

  1. Id——插入的每一行的db递增值
  2. ThreadId-String它保存插入到数据库中的线程的名称。
  3. Created-DateTime因此,您可以看到该行的插入时间

这就是我们真正需要的,因为我们只是尝试将大量数据写入Sqlite

我只是调用它ThreadData,然后把它放在一个命名sqliteThreads.Modelnamespace中,因为我在我的项目下创建了一个名为Model的新文件夹。稍后,您将看到在我们的 Program.cs 文件中,我们需要添加如下using语句: using sqliteThreads.Model它看起来像这样:

namespace sqliteThreads.Model;

class ThreadData{

   Int64 Id{get;set;}

   String ThreadId{get;set;}

   DateTime Created{get;set;}

}

每当我添加新的类或代码时,我都会继续构建,所以让我们再做一次。

$ dotnet build

执行此操作时,可能会收到一条警告,指出ThreadId退出构造函数时必须包含非null。它只是想让你知道,在使用值之前,你需要初始化它们。这将在以后处理。

使用初始化构造新的ThreadData对象

让我们翻转到我们的 Program.cs 文件并添加构造一个new ThreadData对象。

将以下代码添加到Program.cs文件中。

(如果需要,可以删除写出Hello, World!语句的原始行,也可以保留它。

using sqliteThreads.Model;

ThreadData td = new ThreadData{ThreadId="Main", Created=DateTime.Now};

Console.WriteLine($"Id: {td.Id}, ThreadId:{td.ThreadId},  Created:{td.Created}");

运行它时,你将看到如下内容:

Id: 0, ThreadId:Main,  Created:3/20/2024 1:53:52 PM

现在,我们知道我们有一些基本数据可以写入我们的Sqlite数据库。

Entity Framework上下文类

为了访问Sqlite数据库并写入记录,我们正在使用实体框架,所以现在我们需要添加一个DBContext类。

我基本上从以下Microsoft教程中复制了代码,并出于我的目的对其进行了更改:EF Core入门[^]

我创建了DbContext类(ThreadDataContext.cs)并将其添加到Model文件夹中。

更少的安装:未使用dotnet-ef工具

但是,我不想让您安装该dotnet-ef工具(根据上面链接的文章的要求来创建数据库),因此我决定通过Nuget(使用Microsoft.Data.Sqlite)添加对Sqlite库的直接引用,这样我们就可以简单地自己创建数据库(如果它不存在)。所有这些工作都是在ThreadDataContext构造函数中完成的,因此无需担心创建数据库。

检查Sqlite DB文件是否存在

运行代码时,上下文类将检查thread.db文件是否存在,如果不存在,它将检查

  1. 创建数据库文件
  2. ThreadData表添加到数据库

这是该ThreadDataContext类的快照,它应该可以相当清楚地说明Sqlite数据库是如何创建的。(请记住,sqlite数据库只是一个文件。)

namespace sqliteThreads.Model;

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using Microsoft.Data.Sqlite;

public class ThreadDataContext : DbContext
{
    // The variable name must match the name of the table.
    public DbSet<threaddata> ThreadData { get; set; }
    
    public string DbPath { get; }

    public ThreadDataContext()
    {
        var folder = Environment.SpecialFolder.LocalApplicationData;
        var path = Environment.GetFolderPath(folder);
        DbPath = System.IO.Path.Join(path, "thread.db");
        Console.WriteLine(DbPath);

        SqliteConnection connection = new SqliteConnection($"Data Source={DbPath}");
        // ########### FYI THE DB is created when it is OPENED ########
        connection.Open();
        SqliteCommand command = connection.CreateCommand();
        FileInfo fi = new FileInfo(DbPath);
        // check to see if db file is 0 length, if so, it needs to have table added
        if (fi.Length == 0){
            foreach (String tableCreate in allTableCreation){
                command.CommandText = tableCreate;
                command.ExecuteNonQuery();
            }
        }
    }

    // configures the database for use by EF
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite($"Data Source={DbPath}");
    protected String [] allTableCreation = {
        @"CREATE TABLE ThreadData
            (
            [ID] INTEGER NOT NULL PRIMARY KEY,
            [ThreadId] NVARCHAR(30) NOT NULL check(length(ThreadId) <= 30),
            [Created] NVARCHAR(30) default (datetime('now','localtime')) 
                      check(length(Created) <= 30)
            )"
    };

}

添加一些代码到Program.cs并尝试一下

现在,让我们在主程序中添加一些代码并尝试一下。

我们只需要在Program.cs中添加一行代码,该代码将实例化ThreadDataContext类并创建数据库文件。

ThreadDataContext tdc = new ThreadDataContext();

就是这样!现在,让我们运行它。

$ dotnet run

您应该会看到如下内容:

Id: 0, ThreadId:Main,  Created:3/20/2024 2:57:35 PM

/Users/<redacted-user-name>/Library/Application Support/thread.db

注意:我现在正在我的Mac PowerBook上运行它,因此您的路径可能会有所不同。

你有sqlite,不是吗?

现在我们已经创建了数据库并添加了ThreadData表,我们可以使用sqlite3应用程序检查它。

问:您的机器上已经安装了sqlite吗?

:如果您运行的是LinuxmacOS,那么您很可能会这样做。但是,如果你不这样做,你可能需要从sqlite网站获取它[^]。您只需要sqlite3可执行文件,并且可能包含几个DLL,因此您可能想要名称如下:sqlite-tools-win-x64-3450200.zip(包含命令行工具和命令行shell应用程序)。

一旦你运行了上面的应用程序,你就会有一条路径,你将转到一个终端并运行sqlite3命令来打开数据库:

$ sqlite3 "/Users/<redacted-user-name>/Library/Application Support/thread.db"

请注意,我的路径中有空格,因此我被迫在数据库文件的完整路径周围添加双引号。

sqlite3启动后,您将看到一个命令行界面:

继续并键入.schema <ENTER>(请注意该命令前面的前导点):

这允许您查看数据库中的表。

接下来,您可以执行select,但数据库中还没有记录。

select * from ThreadData;

最后,要退出,只需键入.exit(再次注意命令中的前导点)。

现在,让我们去看看如何将记录添加到我们的数据库中。

ThreadData数据库添加数据

在开发环境中打开 Program.cs 文件(你正在使用Visual Studio Code,不是吗?)并添加以下代码:

tdc.Add(td);

tdc.SaveChanges();

Console.WriteLine("Wrote to db");

我们之前已经在名为td的代码中创建了一个ThreadData对象,所以现在我们只需使用我们的ThreadDataContext类的Add()将记录到我们的数据库,然后SaveChanges()写入数据。这就是为什么我决定将Entity Framework用于这个小应用程序的原因。就是这么简单。

运行该代码,每次运行时,都会向数据库添加一条新记录。

我将让您了解如何使用sqlite3连接到数据库并再次运行您的选择。

这是我在运行该应用程序几次后看到的数据。

现在,我们可以做有趣的部分了。让我们生成一些线程,这些线程将同时写入数据库。由于我们提供了一种添加ThreadId的方法,这意味着我们将能够看到哪个线程正在执行工作。

让我们的应用程序在不同的线程上插入数据

在我们的应用程序中创建新线程非常容易。

从字面上看,我们可以生成一个new Thread如下:

Thread t = new Thread(() => Console.WriteLine("I'm on a separate thread!"));

t.Start();

作为测试,继续将这两行添加到Program.cs的顶部,您将在程序中拥有一个新的执行线程。

第一行创建线程,第二行启动线程运行。

根据一本神奇的书,C# 12 In A Nutshell[^],以这种方式创建线程会创建一个前景线程:

C# 12简述

默认情况下,显式创建的线程是前台线程。只要其中任何一个线程在运行,前台线程就会使应用程序保持活动状态,而后台线程则不会。

是的,这意味着如果您生成的线程未完成,则您的应用将不会关闭。

再次运行应用,你将看到如下内容:

Hello, World!

Id: 0, ThreadId:Main,  Created:3/20/2024 3:31:28 PM

/Users/<redacted-username>/Library/Application Support/thread.db

Wrote to db

I'm on a separate thread!

我们的代码运行一个匿名函数(lambda),但我们希望能够传递我们的threadId的名称,因此我们将生成将运行特定函数的线程。现在让我们编写该函数。

用于多个线程的WriteData函数

在编写和测试这种方法时,我学到了很多迂腐的细节,所以我会在这里向你展示它,然后击中高点(和低点)来解释可能不明显的细节。

void WriteData(string threadId){
    ThreadDataContext db = new ThreadDataContext();
    
    for (int i = 0; i < INSERT_COUNT;i++){
        try{
            ThreadData td = new ThreadData{ThreadId=threadId, Created=DateTime.Now};
            db.Add(td);
            db.SaveChanges();
        }
        catch(Exception ex){
            Console.WriteLine($"Error: {threadId} => {ex.InnerException.Message}");
            continue;
        }
    }
}

这个函数有很多要点,我将列出其中的一些可能让你印象深刻的要点,然后我会尝试解释为什么我把它们包括在内。

  1. 我让每个线程在函数的循环中完成所有工作。只要for循环门不满足,每个线程就会保持活动状态。
  2. INSERT_COUNT是一个const int,其允许您设置(Program.cs顶部)每个线程将执行的插入数。
  3. 我在数据库insert期间catch任何故障,然后只是continue循环。通常,这将是由于数据库被大量使用,以至于在此线程尝试insert的确切时刻被锁定。我发现,即使在我的12处理器机器上运行13个线程,这种情况也很少发生。

还有更多的工作需要清理Program.cs但如果你想像我一样在13个线程上运行它,这就是它的样子。

using sqliteThreads.Model;

const int INSERT_COUNT = 100;
int insert_count = 0;

// If user passes number of records as valid integer
// then each thread will insert that number of records
// otherwise program will use INSERT_COUNT
if (args.Length > 0){
    try{
      insert_count = Int32.Parse(args[0]);
    }
    catch{
        insert_count = INSERT_COUNT;
    }
}
else{
    insert_count = INSERT_COUNT;
}

Console.WriteLine($"#### Inserting {insert_count} records for each thread. ####");
Thread t = new Thread(() => WriteData("T1"));
Thread t2 = new Thread(() => WriteData("T2"));
Thread t3 = new Thread(()=>WriteData("T3"));
Thread t4 = new Thread(()=>WriteData("T4"));
Thread t5 = new Thread(()=>WriteData("T5"));
Thread t6 = new Thread(()=>WriteData("T6"));
Thread t7 = new Thread(()=>WriteData("T7"));
Thread t8 = new Thread(()=>WriteData("T8"));
Thread t9 = new Thread(()=>WriteData("T9"));
Thread t10 = new Thread(()=>WriteData("T10"));
Thread t11 = new Thread(()=>WriteData("T11"));
Thread t12 = new Thread(()=>WriteData("T12"));

t.Start();
t2.Start();
t3.Start();
t4.Start();
t5.Start();
t6.Start();

t7.Start();
t8.Start();
t9.Start();
t10.Start();
t11.Start();
t12.Start();

WriteData("Main");

void WriteData(string threadId){
    ThreadDataContext db = new ThreadDataContext();
    var beginTime = DateTime.Now;
    for (int i = 0; i < insert_count;i++){
        try{
            ThreadData td = new ThreadData{ThreadId=threadId, Created=DateTime.Now};
            db.Add(td);
            db.SaveChanges();
        }
        catch(Exception ex){
            Console.WriteLine($"Error: {threadId} => {ex.InnerException.Message}");
            continue;
        }
    }
    Console.WriteLine($"{threadId}: Completed - {DateTime.Now - beginTime}");
}

运行代码

获取代码并试用。我想你会感到惊讶。

如果按原样运行,您会发现ThreadData表中插入了1300条记录。

编辑:使用命令行设置插入计数

我更改了代码并添加了用户在命令行上设置insert_count的功能。

现在,您可以启动应用程序并设置希望每个线程插入的记录数,如下所示:

$ dotnet run <number_of_records>

$ dotnet run 1000

默认值

如果未提供值,则每个线程将运行100次插入。

在运行AMD® Ryzen 5 2600x 六核处理器 × 12LinuxUbuntu 22.04.3 LTS)机器上,我很少看到异常,如下所示:

Error: T10 => $SQLite Error 5: 'database is locked'.

我还在配备36GB内存和M3MacBook Pro上运行了它,并且:

  • 12-core CPU,具有6个性能核心和6个效率核心
  • 18-core GPU
  • 16-core神经网络引擎

我根本没有看到任何锁,这很有趣。

由于Sqlite实际上是一个文件数据库,因此所有的速度都可能基于磁盘/存储设备的速度以及与之关联的缓存。我不确定。

即使有错误,也不会丢失数据

但是,即使我看到出现该错误,我也会发现我不会丢失任何插入物。

我需要多考虑一下,但这真是太神奇了。

添加了一些基本的时序数据

现在,在版本2中,当每个线程启动时,它会抓取beginTime当线程在for循环中完成工作时,它会计算它完成工作所花费的时间。这是非常基本的,但可以让您了解需要多长时间。

用于检查数据的有用查询

Sqlite中尝试以下有用的查询,以便检查数据:

// each run should insert 1300 records
select count(*) from threaddata;

// Get counts grouped by threadId so you can tell if each thread 
// inserted the proper number of times
select threadId, count(*) from threaddata group by threadId;

// take a look at the data and see that each thread does work 
// until it gets context switched and
// another thread starts inserting.
select * from threaddata;

下面是上面第二个查询的结果:它显示每个线程的插入数。

结论

我相信如果你看看这段代码,你会对Sqlite印象深刻。

此外,它的易用性可能会鼓励您开始在自己的项目中使用它。

https://www.codeproject.com/Articles/5379359/How-Much-Can-Sqlite-Handle-Multiple-Threads-Concur

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值