Linux与数据库进阶

事物

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。

为什么需要事物

比如一个银行业务如有借有贷,每有一次借贷业务,需要保证账目上的借方和贷方至少都记上相等的一笔帐,这两笔账要么同时成功,要么同时失败,如果只有一方帐,就会出现记错账的情况.

事物的四大特性

MySQL中的事务(Transaction)是一组操作的集合,这些操作要么全部成功,要么全部失败,是数据库操作的一个基本单位。事务具有四个关键特性,通常被称为ACID特性:

以银行借贷为例:

1.要查看支票账户的余额是否高于转账金额

2.从支票账户余额减去转账金额

3.在储蓄账户余额加上转账金额

原子性

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性

当事物完成时,数据必须处于一致状态,如在事物开始前数据存储数据处于一致状态,当事物执行的途中数据可能处于不同的状态,但是当事物结束后数据依旧要处于同一状态,比如,当第三条语句执行失败以后,因为事物并没有提交,所以并不会保存到数据库中

隔离性

通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有被减去200美元。后面我们讨论隔离级别(Isolationlevel)的时候,会发现为什么我们要说"通常来说"是不可见的。

持久性

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略(如果数据库本身就能做到真正的持久性,那么备份又怎么能增加持久性呢?)。在后面的一些章节中,我们会继续讨论MySQL中持久性的真正含义。

隔离级别

在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

读未提交

读未提交是最低的隔离级别,允许事务读取其他未提交事务的修改(脏读)。这种级别牺牲了数据一致性以换取高并发性能,适用于对数据准确性要求极低但需要快速响应的场景(如实时统计近似值),但在多数业务系统中容易导致数据不一致问题。举个栗子:还是以银行借贷为例,

事物1:A要从账户中拿出1000元给B(未提交)

事物2:查看A的账户余额(直接读到A的账户少了1000)

如果事物1突然回滚,资产没有变化,但是事物B已经读到数据并使用了,这就是脏读.

读已提交

读已提交要求事务只能读取其他已提交事务的数据,避免了脏读,但仍可能发生不可重复读(同一事务多次读取结果不同)。这是多数数据库的默认隔离级别(如Oracle),适合允许短期数据变化的场景(如账户余额查询),平衡了性能与一致性需求。

可重复读

可重复读确保同一事务中多次读取同一数据的结果一致,避免了不可重复读,但仍可能发生幻读(范围查询结果变化)。MySQL默认采用此级别并通过"间隙锁"减少幻读。适用于需要事务内数据稳定的场景(如对账操作),但可能增加锁竞争。

串行化(Serializable)

串行化是最严格的隔离级别,通过强制事务串行执行来彻底避免脏读、不可重复读和幻读。其代价是并发性能显著下降,通常仅用于对数据一致性要求极高的场景(如金融交易核验)。实际应用中需谨慎权衡,避免系统吞吐量瓶颈。

查看当前会话隔离级别

sql 复制代码
mysql> SELECT @@SESSION.transaction_isolation;
 +---------------------------------+
 | @@SESSION.transaction_isolation |
 +---------------------------------+
 | REPEATABLE-READ                 |
 +---------------------------------+
 1 row in set (0.00 sec)
 mysql>

查看系统隔离级别

sql 复制代码
mysql> SELECT @@GLOBAL.transaction_isolation;
 +--------------------------------+
 | @@GLOBAL.transaction_isolation |
 +--------------------------------+
 | REPEATABLE-READ                |
 +--------------------------------+
 1 row in set (0.00 sec)
 mysql>

设置隔离级别

sql 复制代码
# 设置全局隔离级别
set global transaction isolation level REPEATABLE READ;
 set global transaction isolation level  READ COMMITTED;
 set global transaction isolation level READ UNCOMMITTED;
 set global transaction isolation level SERIALIZABLE;
 #设置会话隔离级别
set session transaction isolation level REPEATABLE READ;
 set session transaction isolation level READ COMMITTED;
 set session transaction isolation level READ UNCOMMITTED;
 set session transaction isolation level SERIALIZABLE;

C语言链接数据库---一种更安全的方式

在之前的学习中我们学习的注入语句mysql_query(),一般来说我们写成这个样子:

sql 复制代码
// 传统拼接 SQL 的写法(不安全)
char sql[200];
sprintf(sql, "insert into empolyees(name, em_id, dp) values('%s', '%s', '%s')", name, em_id, dp);
// 执行 SQL
mysql_query(conn, sql);

但是倘若有人向我们的系统中恶意输入一些构造后的sql语句如:

sql 复制代码
张三'); delete from empolyees where 1=1; --

那么我们的语句就会变成这样

sql 复制代码
insert into empolyees(name, em_id, dp) values('张三'); delete from empolyees where 1=1; -- ', '123', '技术部');

这样执行结果就会变成先向数据库中插入一条张三,然后删除所有的员工信息

这时我们就可以使用预处理语句来使用我们的sql语句

预处理语句

预处理语句(也译作 "预编译语句")是数据库编程中一种安全、高效的 SQL 执行方式,核心思想是将 SQL 语句的 "结构解析" 和 "参数赋值" 分两步进行,而非传统的一次性拼接执行,其流程如下图

初始化预处理指令

cpp 复制代码
MYSQL_STMT * stmt=mysql_stmt_init(conn);

发送 SQL 模板并解析

cpp 复制代码
const char *sql = "insert into empolyees(name, em_id, dp) values(?,?,?)";
mysql_stmt_prepare(stmt, sql, strlen(sql)); // 发送模板到数据库解析

绑定并发送参数

cpp 复制代码
mysql_stmt_bind_param(stmt, bind); // 将用户输入的参数绑定到占位符

执行语句

cpp 复制代码
mysql_stmt_execute(stmt); // 执行填充了参数的SQL

预处理语句的核心优势

1**. 彻底杜绝 SQL 注入(最核心优势)**

这是预处理语句最关键的价值:

传统拼接 SQL:用户输入的 张三'); delete from empolyees; -- 会被当作 SQL 语法执行;

预处理语句:无论参数中包含什么字符,数据库都只会将其视为 "字符串数据",而非 SQL 指令,从根源上阻断注入。

  1. 执行效率更高(批量操作时

如果需要多次执行结构相同、参数不同的 SQL(比如批量插入 1000 条员工数据):

传统方式:每条 SQL 都要重新解析语法、生成执行计划,耗时久;

预处理语句:仅解析 1 次模板,后续只需传参数执行,节省大量解析时间。

3.简化参数处理

无需手动转义特殊字符(比如把 ' 转成 ''),数据库会自动处理参数中的特殊字符,避免手动转义遗漏导致的安全问题。
预处理语句的适用场景

所有需要接收用户输入的 SQL 操作(增 / 删 / 改 / 查),尤其是插入、更新、查询;

批量执行相同结构的 SQL(比如批量导入数据);

对安全性要求高的场景(如用户登录、数据写入、敏感信息查询)。

例子

下面以实现往员工信息表中插入语句为例:

cpp 复制代码
int add_empolyees()
{
   //声明函数变量
   MYSQL *conn;          //数据库连接对象
   MYSQL_STMT * stmt;    //预处理语句对象
   MYSQL_BIND bind[3];   //参数绑定数组

   //用户输入缓冲区
   char name[50];
   char em_id[50];
   char dp[50];

   //声明字符串长度变量
   unsigned long name_len;
   unsigned long em_id_len;
   unsigned long dp_len;

   printf("----添加员工界面----\n");
   
   //获取员工姓名
   printf("请输入员工姓名:\n");
   gets();
   fgets(name, sizeof(name), stdin);
   name[strcspn(name ,"\n")] = 0;  //移除末尾的换行符

   //获取员工姓名
   printf("请输入员工工号:\n");
   fgets(em_id, sizeof(em_id), stdin);
   em_id[strcspn(em_id ,"\n")] = 0;  //移除末尾的换行符

   //获取员工姓名
   printf("请输入员工部门:\n");
   fgets(dp, sizeof(dp), stdin);
   dp[strcspn(dp ,"\n")] = 0;  //移除末尾的换行符

   //建立数据库的连接
   conn = connect_databse();
   if(conn == NULL)
    return 1;

   //定义sql语句,使用?作为参数占位符
   const char *sql = "insert into empolyees(name, em_id, dp) values(?,?,?)";
   //初始化预处理语句
   stmt = mysql_stmt_init(conn);
   //准备预处理语句,发送给数据库进行解析
   if(mysql_stmt_prepare(stmt, sql, strlen(sql)) !=0 )
     {
        printf("预处理语句准备失败\n");
        //close_database();
        return 1;
     }
    
    //将参数绑定数组清空
    memset(bind ,0 ,sizeof(bind));
    
    //绑定姓名信息
    name_len = strlen(name);
    bind[0].buffer_type = MYSQL_TYPE_STRING; //指定参数类型为字符串
    bind[0].buffer = name;  //指定数据缓冲区
    bind[0].buffer_length = sizeof(name);   //指定数据缓冲区大小
    bind[0].length = &name_len; //指定字符串长度指针

     //绑定工号信息
    em_id_len = strlen(em_id);
    bind[1].buffer_type = MYSQL_TYPE_STRING; //指定参数类型为字符串
    bind[1].buffer = em_id;  //指定数据缓冲区
    bind[1].buffer_length = sizeof(em_id);   //指定数据缓冲区大小
    bind[1].length = &em_id_len; //指定字符串长度指针

     //绑定部门信息
    dp_len = strlen(dp);
    bind[2].buffer_type = MYSQL_TYPE_STRING; //指定参数类型为字符串
    bind[2].buffer = dp;  //指定数据缓冲区
    bind[2].buffer_length = sizeof(dp);   //指定数据缓冲区大小
    bind[2].length = &dp_len; //指定字符串长度指针


    //将参数设置到预处理语句中
    if(mysql_stmt_bind_param(stmt, bind) !=0)
    {
        printf("参数绑定失败\n");
        mysql_stmt_close(stmt);//关闭预处理语句
        //close_database();
        return 1;
    }

    //将准备好的预处理语句发送给数据库实现功能
    if(mysql_stmt_execute(stmt) !=0)
    {
        printf("预处理语句插入失败\n");
        mysql_stmt_close(stmt);//关闭预处理语句
        //close_database();
        return 1;
    }

    printf("员工信息添加成功!\n");
    //释放资源
    mysql_stmt_close(stmt);//关闭预处理语句
    //close_database();
    return 0;
}
相关推荐
程序员阿鹏26 分钟前
如何实现MySQL分库分表?
数据结构·数据库·sql·mysql·缓存
风哥2号43 分钟前
数据库教程FGMT33‑MySQL主从复制项目实施与维护06(MySQL8.4/9.7 MGR组复制)
数据库·mysql
liangsheng_g1 小时前
Spring事务传播行为分派与挂起恢复源码实战
数据库·sql·spring
蓝速科技1 小时前
信创终端 POC 测试实战与选型避坑指南丨蓝速科技
运维·数据库·人工智能·科技·自然语言处理
风哥2号1 小时前
数据库教程FGMT29‑Linux平台MySQL5.7安装配置与管理入门
数据库
杨云龙UP2 小时前
MySQL Host is blocked because of many connection errors 导致 JDBC 连接失败排查与解决
linux·运维·网络·数据库·sql·mysql·登录失败
这个DBA有点耶3 小时前
数据库数据同步解决方案怎么选?6款主流工具横向对比与信创选型指南
数据库·架构·dba
刃神太酷啦4 小时前
Redis 核心进阶:哨兵、集群、缓存问题与分布式锁详解----《Hello Redis!》(6)
linux·c语言·数据库·c++·redis·分布式·缓存
小雷信息医学4 小时前
不会写复杂代码也能发 SCI?手把手教你用 InSpireR 交互系统一键提取、合并与导出临床科研宽表【第三章】
数据库
旺仔不是程序员4 小时前
复合索引最左前缀原则:PostgreSQL 的 WHERE 为什么必须命中第一列
数据库·后端·sql