前两天在网上看到一个程序员兄弟谈他的面试经,其中讲了他遇到的一个问题:
给了一张表 三个字段:
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"+" 插入完成"); } }
测试类:Main.java
public class Main { public static void main(String[] args) { for (int i = 1; i <=10; i++) { new MyThread().start(); } }
运行结果:成功插入了,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"+" 插入完成"); } }
测试代码:
Main.java
public class Main { public static void main(String[] args) { for (int i = 1; i <=10; i++) { new MyThread().start(); } } }
结果:速度比第一次快了很多:
关于这个面试题,大家如果有好的方法,请尽情提供,大家一起学习下!!
另外关于高并发情况下的线程安全,大家有什么看法?望一起讨论!