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

如何实现命令模式的线程安全?多进程共享ArrayList存审计日志场景

Thread-Safe Audit Logging with Command Pattern

Great question! Let's break down how to fix the thread-safety issue in your audit logging system using the Command Pattern, addressing the shared ArrayList problem and ensuring safe concurrent access across threads/processes.

Core Idea

The Command Pattern helps us encapsulate each audit log entry as an immutable command object. Combined with thread-safe data structures and proper separation of concerns, we eliminate race conditions from shared mutable state.

Step 1: Define an Immutable Audit Command Interface

First, create an abstract command that encapsulates the audit action and data. Make sure command objects are immutable (no setters, all fields final) so they're inherently thread-safe.

public interface AuditCommand {
    // Execute any pre-processing logic for the audit entry
    void execute();
    // Retrieve the finalized audit data for DB persistence
    AuditData getAuditData();
}

Step 2: Implement Concrete Audit Commands

Create specific command implementations for different audit events. Since these objects are immutable, multiple threads can access them without risk of concurrent modification.

public class UserActionAuditCommand implements AuditCommand {
    private final AuditData auditData;

    // Initialize all audit data in the constructor (no changes after creation)
    public UserActionAuditCommand(String userId, String action, LocalDateTime timestamp) {
        this.auditData = new AuditData(userId, action, timestamp);
    }

    @Override
    public void execute() {
        // Optional: Validate data format, generate metadata, etc.
        // Since auditData is immutable, this logic is thread-safe
    }

    @Override
    public AuditData getAuditData() {
        return auditData;
    }
}

Step 3: Replace Shared ArrayList with a Thread-Safe Command Queue

Instead of using a non-thread-safe ArrayList, use a concurrent queue like ConcurrentLinkedQueue (for unbounded queues) or ArrayBlockingQueue (for bounded queues). These collections handle thread-safe adds/removes without manual locking.

public class AuditCommandExecutor {
    // Thread-safe queue to hold audit commands
    private final Queue<AuditCommand> auditCommandQueue = new ConcurrentLinkedQueue<>();
    // Receiver responsible for DB persistence logic
    private final AuditDbPersistenceReceiver dbReceiver;

    public AuditCommandExecutor(AuditDbPersistenceReceiver dbReceiver) {
        this.dbReceiver = dbReceiver;
    }

    // Log an audit event by adding a command to the queue
    public void logAudit(AuditCommand command) {
        command.execute(); // Run pre-processing (safe, since command is immutable)
        auditCommandQueue.add(command); // Thread-safe add operation
    }

    // Called when a process/thread ends to persist all pending audit logs
    public void run() {
        // Batch fetch all pending commands to minimize DB roundtrips
        List<AuditCommand> pendingCommands = new ArrayList<>();
        AuditCommand cmd;
        while ((cmd = auditCommandQueue.poll()) != null) {
            pendingCommands.add(cmd);
        }

        if (!pendingCommands.isEmpty()) {
            // Convert commands to DB-ready data
            List<AuditData> auditDataList = pendingCommands.stream()
                    .map(AuditCommand::getAuditData)
                    .collect(Collectors.toList());
            // Delegate to receiver for batch DB save
            dbReceiver.batchSave(auditDataList);
        }
    }
}

Step 4: Implement the DB Persistence Receiver

The receiver handles the actual database operations. Ensure batch saves are thread-safe (use connection pools, proper transaction isolation, etc.).

public class AuditDbPersistenceReceiver {
    public void batchSave(List<AuditData> auditDataList) {
        // Use a thread-safe DB connection pool for concurrent access
        try (Connection conn = DatabaseConnectionPool.getConnection()) {
            conn.setAutoCommit(false);
            // Execute bulk insert logic (e.g., JDBC batch updates)
            String sql = "INSERT INTO audit_logs (user_id, action, timestamp) VALUES (?, ?, ?)";
            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
                for (AuditData data : auditDataList) {
                    stmt.setString(1, data.getUserId());
                    stmt.setString(2, data.getAction());
                    stmt.setTimestamp(3, Timestamp.valueOf(data.getTimestamp()));
                    stmt.addBatch();
                }
                stmt.executeBatch();
            }
            conn.commit();
        } catch (SQLException e) {
            // Handle exceptions (rollback, logging, etc.)
            e.printStackTrace();
        }
    }
}

Key Thread-Safety Guarantees

  • Immutable Command Objects: No concurrent modification of audit data since commands can't be changed after creation.
  • Thread-Safe Queue: ConcurrentLinkedQueue handles atomic adds/removes, eliminating race conditions from shared collection access.
  • Batch Processing: Fetching all pending commands at once minimizes lock contention and reduces DB overhead.
  • Isolated DB Operations: The receiver manages DB connections and transactions safely for concurrent persistence calls.

Note for Multi-Process Scenarios

If you're working across separate processes (not just threads), shared in-memory queues won't work (processes don't share memory). Instead:

  • Use a distributed message queue (e.g., Redis List, Kafka) to store audit commands.
  • Each process sends commands to the queue, and a dedicated worker process runs the run() logic to persist to the DB.

内容的提问来源于stack exchange,提问作者Soumyajit Swain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 11:59:04