如何处理关系数据库上的并发操作?

huangapple go评论64阅读模式
英文:

How to handle concurrent operations on relational databases?

问题

我有一个客户端/服务器应用程序(本地),其中多个客户端同时在服务器上执行操作。服务器为每个客户端创建一个单独的线程,在其中处理所需的操作(注册/登录)。每个线程都有其自己的数据库连接,并通过从ServerSocket.accept()获取的独立套接字与客户端进行通信。客户端的操作需要对服务器上的数据库(sqlite)进行读写访问。注册操作检查条目是否已存在,如果不存在则将其添加。登录操作仅检查密码。然后,线程将告诉客户端操作是否成功。

当客户端完成时,它向服务器发送消息以关闭连接。服务器上的线程中断其循环,然后关闭其套接字和与数据库的连接。

当只有一个客户端时,程序没有问题。如果有多个客户端同时进行操作,那么我会得到两种类型的SQLExceptions,"数据库被锁定"和"唯一约束失败"。前者可能发生,因为数据库上正在执行并发事务。这是因为ACID的原因而被阻止。后者可能发生,因为存在并发性(在将新条目插入DB之前,我检查主键是否已被使用,主键是用户名)。如果我错了,请纠正我。

现在,在对数据库执行操作时,我没有使用线程同步或禁用自动提交。

我认为在ACID数据库上的事务在当前事务尚未完成时自动排队,如果调用了新事务,但这似乎并不是情况。所以,我是否应该在执行查询/更新时通过使用线程同步(例如使用锁)来主动防止这些异常,或者只是重试直到操作成功?处理这些异常的常见方法是什么?我知道线程等待消息结束是有问题的,但这是另一个话题。这只是一个学习处理数据库的程序。

以下是运行在服务器线程上的代码片段。当使用此可运行代码创建一个线程时,我从ServerSocket.accept()创建一个新的Socket,并从驱动程序创建一个新的Connection对象,然后将两者都传递给它。

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;
    }
} // 类结束

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;
}
英文:

I have a client / server application (local) where several clients perform operations concurrently on the server. The server creates a separate thread for each client in which the desired operations (register/login) are then processed. Each thread has its own connection to the database and communicates with the client via its own socket that i get from ServerSocket.accept(). The operations from the client require read and write access to a database (sqlite) on the server. The register op checks whether the entry already exists, if not it gets added. And the login op is just checking passwords. The thread will then tell the client whether the operation was successful or not.
When a client is finished, it sends a message to the server to close the connection. The thread on the server interrupts its loop and then closes its socket and its connection to the database.

When there is just one client, there are no problems with the program. If There are multiple clients concurrently, then i get 2 types of SQLExceptions, "database is locked" and "unique constraint failed". The former might happen, because there are concurrent transactions being performed on the database. And this is prevented, because of ACID. The latter might happen, because of concurrency (before inserting a new entry into the DB I check whether the primary key is already used, the primary key is a username). Correct me if im wrong.
Right now I am not using thread synchronization or disabling auto commit when doing the operations on the database.

I thought that the transactions on ACID DBs are automatically queued up if new ones are invoked while the current one has not finished yet. But this does not seem to be the case. So am I supposed to actively prevent these exceptions by using thread synchronization (e.g. by using Locks) when executing queries/updates or perhaps just retry until the operations succeed? What are common approaches to handle the exceptions? I know that its problematic that the thread wait for a message to end but this is another topic. This is just a program to learn about working with databases.

Here are some snippets of code that runs on the server's threads. When a thread using this runnable is created I create a new Socket from ServerSocket.accept() and a new Connection object from the driver and pass both to it.

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
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

得分: 2

不应该为此使用线程同步。有很多原因这将永远不起作用。

您是否显式地使用begin()和commit()来启动和提交每个事务?

但听起来你的事务运行时间太长。在与客户端通过网络通信时,您不应该永远保持事务处于打开状态。所以您应该做的是:

从客户端获取所有所需的数据。在数据库上调用Begin()。
在数据库上执行所有必要的SQL操作。
然后commit()。
然后将回复发送回客户端,以便事务尽可能短暂地保持打开状态。

更新:
这还取决于事务隔离级别。在sqllite中有这些描述:

另外:您可以将sqllite设置为独占模式,这对您来说可能是一个不错的测试。如果您这样做,事务开始挂起,那么您的问题可能是您有未关闭的事务。

请参见:
https://dba.stackexchange.com/questions/258003/what-standard-sql-isolation-levels-do-the-sqlite-isolation-levels-correspond-to

英文:

No you should not thread synchronization for this. There are many reasons this will newer work.

Do you explicit start and commit each transactions with begin() and commit()?

But it sounds to me like you have transactions running for to long. You should newer have a transaction open, while communication with the client over the network. So what you should do is:

Get all the data you need from the client. Call Begin() on the database.
Do all needed sql on the database.
Then commit()
and then send the reply back to the client so the transaction is open as little time as possible.

Updated:
This also depend on the transaction isolation level. There is description of these is sqllite here:

Btw: You can set sqllite to work in exclusive mode, which might be a good test for you. If you do that and transactions begin to hang, then your problem is that you have open transactions which you don't close.

see:
https://dba.stackexchange.com/questions/258003/what-standard-sql-isolation-levels-do-the-sqlite-isolation-levels-correspond-to

huangapple
  • 本文由 发表于 2023年2月18日 02:05:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/75487831.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定