由一个面试题引发的思考

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 前两天在网上看到一个程序员兄弟谈他的面试经,其中讲了他遇到的一个问题:给了一张表 三个字段:id name dateid为主键现在要求用10个线程向这张表中插入10000条数据 其中id不能是自增,请问该如何实现?能大致说一下思路么?这样一个问题怎么处理?是不是似曾相识呢?我们肯定在哪里见过。

前两天在网上看到一个程序员兄弟谈他的面试经,其中讲了他遇到的一个问题:

给了一张表 三个字段:
id name date
id为主键
现在要求用10个线程向这张表中插入10000条数据 
其中id不能是自增,请问该如何实现?能大致说一下思路么?

这样一个问题怎么处理?

是不是似曾相识呢?我们肯定在哪里见过。

说点题外话:为什么不用自增?这里面试官的意图是想考察线程安全方面的处理

  对大型的系统自增会存在以下问题:

  1) 比如做数据库优化,在大表做水平分表时,就不能使用自增Id,因为Insert的记录插入到哪个分表依分表规则判定决定,若是自增Id,各个分表中Id就会重复,在做查询、删除时就会有异常。

  2) 在对表进行高并发单记录插入时需要加入事物机制,否则会出现Id重复的问题。

  3) 在业务上操作父、子表(即关联表)插入时,需要在插入数据库之前获取max(id)用于标识父表和子表关系,若存在并发获取max(id)的情况,max(id)会同时被别的线程获取到。

  4)  可能面临特定的订单号等等需求。

网上查了下,大家推荐使用AtomicInteger类的addAndGet方法来实现唯一标示,这样可是实现“线程安全”。

然后,给出1.0的方案:10个线程,每个线程插入1000条数据,那么整体就是10000条数据了,满足要求

MyTread.java

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

public class MyThread extends Thread{
    private static AtomicInteger IntId = new AtomicInteger(0);

    public static int getIntId()
    {
        return ((int)(IntId.addAndGet(1)));
    }
    public void run() {
        String url = "jdbc:mysql://127.0.0.1/bigdatatest";
        String name = "com.mysql.jdbc.Driver";
        String user = "root";
        String password = "";
        Connection conn = null;
        try {
            Class.forName(name);
            conn = DriverManager.getConnection(url, user, password);//获取连接
            conn.setAutoCommit(false);//关闭自动提交,不然conn.commit()运行到这句会报错
        } catch (ClassNotFoundException e1) {
            e1.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 开始时间
        Long begin = new Date().getTime();
        // sql前缀
        String prefix = "INSERT INTO test (id,name,data) VALUES ";
        try {
            // 保存sql后缀
            StringBuffer suffix = new StringBuffer();
            // 设置事务为非自动提交
            conn.setAutoCommit(false);
            // 比起st,pst会更好些
            PreparedStatement  pst = (PreparedStatement) conn.prepareStatement("");//准备执行语句
            // 外层循环,总提交事务次数
            for (int i = 1; i <= 1000; i++) {
                suffix = new StringBuffer();
                    String uuid= UUID.randomUUID().toString();
                    int intId = getIntId();
                    // 构建SQL后缀
                    suffix.append("('"+intId+"','" +uuid+"','"+uuid+"'),");
                // 构建完整SQL
                String sql = prefix + suffix.substring(0, suffix.length() - 1);
                // 添加执行SQL
                pst.addBatch(sql);
                // 执行操作
                pst.executeBatch();
                // 提交事务
                conn.commit();
                // 清空上一次添加的数据
                suffix = new StringBuffer();
            }
            // 头等连接
            pst.close();
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 结束时间
        Long end = new Date().getTime();
        // 耗时
        System.out.println("1000条数据插入花费时间 : " + (end - begin) / 1000 + " s"+"  插入完成");
    }
}
View Code

测试类:Main.java

public class Main {

    public static void main(String[] args) {
        for (int i = 1; i <=10; i++) {
            new MyThread().start();
        }
}
View Code

运行结果:成功插入了,AtomicInteger类的addAndGet方法实现乐唯一标示,但是速度较慢。

看看程序好像可以优化下,上面的代码是提交了1000次,这样效率比较慢,可以改进成分成10组,先拼装然后提交:

MyThread.java

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

public class MyThread extends Thread{
    private static AtomicInteger IntId = new AtomicInteger(0);

    public static int getIntId()
    {
        return ((int)(IntId.addAndGet(1)));
    }
    public void run() {
        String url = "jdbc:mysql://127.0.0.1/bigdatatest";
        String name = "com.mysql.jdbc.Driver";
        String user = "root";
        String password = "";
        Connection conn = null;
        try {
            Class.forName(name);
            conn = DriverManager.getConnection(url, user, password);//获取连接
            conn.setAutoCommit(false);//关闭自动提交,不然conn.commit()运行到这句会报错
        } catch (ClassNotFoundException e1) {
            e1.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 开始时间
        Long begin = new Date().getTime();
        // sql前缀
        String prefix = "INSERT INTO test (id,name,data) VALUES ";
        try {
            // 保存sql后缀
            StringBuffer suffix = new StringBuffer();
            // 设置事务为非自动提交
            conn.setAutoCommit(false);
            // 比起st,pst会更好些
            PreparedStatement  pst = (PreparedStatement) conn.prepareStatement("");//准备执行语句
            // 外层循环,总提交事务次数
            for (int i = 1; i <= 10; i++) {
                suffix = new StringBuffer();
                // 第j次提交步长
                for (int j = 1; j <= 100; j++) {
                    String uuid= UUID.randomUUID().toString();
                    int intId = getIntId();
                    // 构建SQL后缀
                    suffix.append("('"+intId+"','" +uuid+"','"+uuid+"'),");
                }
                // 构建完整SQL
                String sql = prefix + suffix.substring(0, suffix.length() - 1);
                // 添加执行SQL
                pst.addBatch(sql);
                // 执行操作
                pst.executeBatch();
                // 提交事务
                conn.commit();
                // 清空上一次添加的数据
                suffix = new StringBuffer();
            }
            // 头等连接
            pst.close();
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 结束时间
        Long end = new Date().getTime();
        // 耗时
        System.out.println("1000条数据插入花费时间 : " + (end - begin) / 1000 + " s"+"  插入完成");
    }
}
View Code

测试代码:

Main.java

public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <=10; i++) {
            new MyThread().start();
        }
    }
}
View Code

结果:速度比第一次快了很多:

关于这个面试题,大家如果有好的方法,请尽情提供,大家一起学习下!!

另外关于高并发情况下的线程安全,大家有什么看法?望一起讨论!

 

 

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
NoSQL 关系型数据库 MySQL
面试题30天打卡-day11
面试题30天打卡-day11
49 0
|
存储 XML Java
面试题30天打卡-day13
面试题30天打卡-day13
45 0
|
NoSQL Java 编译器
面试题30天打卡-day26
面试题30天打卡-day26
48 0
|
消息中间件 存储 NoSQL
面试题30天打卡-day23
面试题30天打卡-day23
34 0
|
Cloud Native 关系型数据库 MySQL
面试题30天打卡-day18
面试题30天打卡-day18
42 0
|
6月前
|
存储 算法 编译器
C++面试题其一
C++文件编译与执行的四个阶段 预处理:处理#include、#define等预处理指令。 编译:将源码翻译为目标代码。 汇编:将目标代码转换为机器指令。 链接:将目标文件和库文件合并生成可执行文件。 STL中的vector的实现,是怎么扩容的? vector通过动态数组实现,当容量不足时,分配更大的内存(通常是原来的两倍),复制旧数据到新内存,并释放旧内存。
86 2
|
6月前
|
存储 程序员 编译器
C++面试题其二
extern "C" 用于告诉编译器按照C语言的链接方式处理代码,通常用于C++代码与C代码混合编程,以防止因名字修饰(name mangling)引起的链接错误。例如: extern "C" { void c_function(); } 通过这些问题的深入理解和解答,能够更好地掌握C++编程的核心概念和实际应用,为面试做好充分的准备。
79 1
|
前端开发 JavaScript 开发工具
【面试题2】2
【面试题2】
|
7月前
|
缓存 小程序 Java
【面试题】1、总结面试题1
【面试题】1、总结面试题1
62 0
|
存储 NoSQL 关系型数据库
面试题30天打卡-day20
面试题30天打卡-day20
62 0