如何处理关系型数据库并发操作?SQLite并发异常解决指南
问题描述
我开发了一个本地C/S应用,支持多客户端并发向服务器发起操作。服务器为每个客户端创建独立线程处理注册、登录等操作,每个线程持有独立的数据库Connection和从ServerSocket.accept()获取的Socket。客户端操作涉及SQLite数据库的读写:
- 注册操作:先检查用户名(主键)是否存在,不存在则插入新用户
- 登录操作:仅验证密码正确性
单客户端运行正常,但多客户端并发时出现两种SQLException:
- "database is locked":并发事务导致数据库锁定,受SQLite的ACID特性限制
- "unique constraint failed":注册时的「检查用户名存在性」和「插入新用户」之间存在竞态,导致重复插入主键
当前未使用线程同步,数据库操作也未关闭自动提交。原本以为ACID数据库会自动排队未完成事务,但实际并非如此。想知道:
- 是否需要通过线程同步(如Locks)执行查询/更新,或通过重试操作避免异常?
- 处理这类异常的常见方案有哪些?
(注:线程等待消息关闭的问题暂不讨论,此程序用于学习数据库操作)
代码片段
ConnectionHandler类
public class ConnectionHandler implements Runnable { private final Socket socket; private final Connection dbConnection; public ConnectionHandler(Socket socket, Connection databaseConnection) { this.socket = socket; this.dbConnection = databaseConnection; } @Override public void run() { try (socket; dbConnection; ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); ) { boolean shouldClose = false; while (!shouldClose) { Actions action = (Actions) ois.readObject(); boolean success = false; if (action == Actions.REGISTER) { success = performClientRegistration(ois, dbConnection); } else if (action == Actions.LOGIN) { success = performClientLogin(ois, dbConnection); } else if(action == Actions.SHUTDOWN) { shouldClose = true; } if (success) { oos.writeObject(Response.SUCCESS); } else { oos.writeObject(Response.FAILURE); } } } catch (ClassNotFoundException | IOException | SQLException e) { e.printStackTrace(); } System.out.println("Socket to " + socket + " closed."); } private boolean performClientRegistration(ObjectInputStream ois, Connection dbConnection) throws IOException, ClassNotFoundException { String userName = (String)ois.readObject(); String pwd = (String)ois.readObject(); String firstName = (String)ois.readObject(); String lastName = (String)ois.readObject(); try { boolean isClientRegistrationPossible = !DBHelper.existsEntry(dbConnection, userName); if(isClientRegistrationPossible) { DBHelper.addEntry(dbConnection, userName, pwd, firstName, lastName); return true; } } catch (SQLException e) { e.printStackTrace(); } return false; } } // END OF CLASS
DBHelper工具类相关方法
public static void addEntry(Connection connection, String userName, String pwd, String firstName, String lastName) throws SQLException { PreparedStatement statement = connection.prepareStatement("INSERT INTO users " + "(userName, pwd, lastName, firstName) VALUES (?, ?, ?, ?)"); statement.setString(1, userName); statement.setString(2, pwd); statement.setString(3, lastName); statement.setString(4, firstName); statement.executeUpdate(); } public static boolean existsEntry(Connection connection, String userName) throws SQLException { PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM users WHERE userName = ?"); statement.setString(1, userName); ResultSet result = statement.executeQuery(); if(result.next()) { return true; } return false; }
解决方案
1. 解决「unique constraint failed」:原子化数据库操作消除竞态
当前注册流程的「查询+插入」是两个独立操作,并发场景下必然出现竞态。最可靠的方式是将两步合并为一个原子操作:
- 使用
INSERT OR IGNORE语句:直接尝试插入用户,若用户名已存在则忽略操作,无需提前查询
修改addEntry方法:
同时简化public static boolean addEntry(Connection connection, String userName, String pwd, String firstName, String lastName) throws SQLException { PreparedStatement statement = connection.prepareStatement("INSERT OR IGNORE INTO users " + "(userName, pwd, lastName, firstName) VALUES (?, ?, ?, ?)"); statement.setString(1, userName); statement.setString(2, pwd); statement.setString(3, lastName); statement.setString(4, firstName); int affectedRows = statement.executeUpdate(); // 受影响行数为1表示插入成功,0表示用户名已存在 return affectedRows == 1; }performClientRegistration方法,直接调用addEntry并根据返回值判断结果,去掉提前查询步骤。
2. 解决「database is locked」:事务控制+重试机制
SQLite写操作会独占锁,并发写时会抛出锁定异常,可通过以下方式处理:
- 手动控制事务:关闭自动提交,将数据库操作包裹在事务中,缩短锁持有时间
在performClientRegistration中调整逻辑:try { dbConnection.setAutoCommit(false); boolean success = DBHelper.addEntry(dbConnection, userName, pwd, firstName, lastName); dbConnection.commit(); return success; } catch (SQLException e) { dbConnection.rollback(); throw e; } finally { dbConnection.setAutoCommit(true); // 恢复自动提交 } - 添加重试逻辑:捕获「database is locked」异常,等待一段时间后重试,直到成功或达到重试上限
示例:private boolean performClientRegistration(ObjectInputStream ois, Connection dbConnection) throws IOException, ClassNotFoundException { String userName = (String)ois.readObject(); String pwd = (String)ois.readObject(); String firstName = (String)ois.readObject(); String lastName = (String)ois.readObject(); int retryCount = 3; while (retryCount > 0) { try { dbConnection.setAutoCommit(false); boolean success = DBHelper.addEntry(dbConnection, userName, pwd, firstName, lastName); dbConnection.commit(); return success; } catch (SQLException e) { dbConnection.rollback(); if (e.getMessage().contains("database is locked") && retryCount > 1) { retryCount--; try { Thread.sleep(500); // 等待500ms后重试 } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } else { e.printStackTrace(); return false; } } finally { try { dbConnection.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } } return false; }
3. 线程同步(可选学习方案)
若不想依赖数据库原子操作,可在服务器层面添加全局锁,确保同一时间只有一个线程执行注册操作:
- 定义全局锁对象:
private static final Object REGISTRATION_LOCK = new Object(); - 在
performClientRegistration中加锁:
此方式会降低并发性能,仅适合学习阶段理解线程同步的作用,不推荐生产环境使用。synchronized (REGISTRATION_LOCK) { // 原有查询+插入逻辑 }
总结
- 优先使用数据库原子操作解决主键重复问题,这是最可靠且性能较好的方案
- 结合事务控制+重试机制处理数据库锁定异常,适配SQLite的锁特性
- 线程同步作为备选学习方案,实际场景中不如数据库层面的方案可靠
内容的提问来源于stack exchange,提问作者Neran
相关产品推荐
相关产品推荐

