• 由一个面试题引发的思考


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

    给了一张表 三个字段:
    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

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

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

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

    
    
    
  • 相关阅读:
    关于migration build failed的问题
    C盘无损扩容(傻逼拯救者128G固态分两个盘)
    .NET Core:搭建私有Nuget服务器以及打包发布Nuget包
    VMware下的Centos7联网并设置固定IP(nat)
    使用docker compose 构建多个镜像
    centos 安装docker-compose
    使用使用dockerfile构建webapi镜像然后使用link和bridge两种方式进行桥接
    docker 安装mysql 并将文件挂载到本地
    Conetos 下安装docker 和镜像加速
    docker 一些命令
  • 原文地址:https://www.cnblogs.com/miketwais/p/Highly_concurrent.html
Copyright © 2020-2023  润新知