You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何处理关系型数据库并发操作?SQLite并发异常解决指南

问题描述

我开发了一个本地C/S应用,支持多客户端并发向服务器发起操作。服务器为每个客户端创建独立线程处理注册、登录等操作,每个线程持有独立的数据库Connection和从ServerSocket.accept()获取的Socket。客户端操作涉及SQLite数据库的读写:

  • 注册操作:先检查用户名(主键)是否存在,不存在则插入新用户
  • 登录操作:仅验证密码正确性

单客户端运行正常,但多客户端并发时出现两种SQLException:

  1. "database is locked":并发事务导致数据库锁定,受SQLite的ACID特性限制
  2. "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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 05:42:26