← Назад к вопросам

Как организовать доступ к общим данным в эндпоинте на Spring, чтобы они были разделяемыми между всеми клиентами и потоками

2.3 Middle🔥 181 комментариев
#REST API и микросервисы#Spring Framework#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Общие данные между клиентами в Spring Controllers

1. Синглтон Bean (рекомендуется)

Используем Spring Bean с scope = singleton (по умолчанию):

import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;

@Component  // Singleton по умолчанию
public class SharedDataService {
    private final ConcurrentHashMap<String, Object> sharedData = new ConcurrentHashMap<>();
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public void setData(String key, Object value) {
        sharedData.put(key, value);
    }
    
    public Object getData(String key) {
        return sharedData.get(key);
    }
    
    public int incrementCounter() {
        return counter.incrementAndGet();
    }
    
    public int getCounter() {
        return counter.get();
    }
}

@RestController
@RequestMapping("/api/shared")
public class SharedDataController {
    private final SharedDataService sharedDataService;
    
    public SharedDataController(SharedDataService sharedDataService) {
        this.sharedDataService = sharedDataService;
    }
    
    @PostMapping("/data")
    public void setData(@RequestParam String key, @RequestParam String value) {
        sharedDataService.setData(key, value);
    }
    
    @GetMapping("/data/{key}")
    public Object getData(@PathVariable String key) {
        return sharedDataService.getData(key);
    }
    
    @PostMapping("/increment")
    public int incrementCounter() {
        return sharedDataService.incrementCounter();
    }
}

2. Потокобезопасные коллекции

Используем ConcurrentHashMap, AtomicInteger, CopyOnWriteArrayList:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import org.springframework.stereotype.Service;

@Service
public class ThreadSafeSharedDataService {
    
    // Потокобезопасная коллекция для хранения данных
    private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
    
    // Потокобезопасный счётчик
    private final AtomicInteger requestCount = new AtomicInteger(0);
    
    // Потокобезопасный список
    private final CopyOnWriteArrayList<String> events = new CopyOnWriteArrayList<>();
    
    // Потокобезопасный флаг
    private final AtomicBoolean isProcessing = new AtomicBoolean(false);
    
    public void addToCache(String key, String value) {
        cache.put(key, value);
    }
    
    public String getFromCache(String key) {
        return cache.get(key);
    }
    
    public int getAndIncrementRequestCount() {
        return requestCount.incrementAndGet();
    }
    
    public void addEvent(String event) {
        events.add(event);
    }
    
    public boolean setProcessing(boolean value) {
        return isProcessing.compareAndSet(!value, value);
    }
}

3. Использование Synchronized блоков

Для критических секций:

import org.springframework.stereotype.Service;

@Service
public class SynchronizedSharedDataService {
    private java.util.HashMap<String, Object> data = new java.util.HashMap<>();
    private int counter = 0;
    
    // Синхронизация для read-write операций
    public synchronized void setData(String key, Object value) {
        data.put(key, value);
    }
    
    public synchronized Object getData(String key) {
        return data.get(key);
    }
    
    public synchronized int incrementCounter() {
        return ++counter;
    }
}

4. ReadWriteLock для оптимизации

Если много читателей, мало писателей:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.springframework.stereotype.Service;

@Service
public class ReadWriteSharedDataService {
    private final java.util.HashMap<String, String> data = new java.util.HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void setData(String key, String value) {
        lock.writeLock().lock();
        try {
            data.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public String getData(String key) {
        lock.readLock().lock();
        try {
            return data.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
}

5. Database для долгосрочного хранения

Для персистентных общих данных:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import javax.persistence.*;

@Entity
@Table(name = "shared_data")
public class SharedData {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String key;
    
    private String value;
    
    // Getters, setters...
}

@Repository
public interface SharedDataRepository extends JpaRepository<SharedData, Long> {
    SharedData findByKey(String key);
}

@Service
public class PersistentSharedDataService {
    private final SharedDataRepository repository;
    
    public PersistentSharedDataService(SharedDataRepository repository) {
        this.repository = repository;
    }
    
    public void setData(String key, String value) {
        SharedData data = repository.findByKey(key);
        if (data == null) {
            data = new SharedData();
            data.setKey(key);
        }
        data.setValue(value);
        repository.save(data);
    }
    
    public String getData(String key) {
        SharedData data = repository.findByKey(key);
        return data != null ? data.getValue() : null;
    }
}

6. Cache (Redis, Caffeine)

Для высокопроизводительного кэширования:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class CachedSharedDataService {
    
    @Cacheable(value = "shared-data", key = "#key")
    public String getData(String key) {
        // Если в кэше нет — запрос к БД или вычисление
        return fetchDataFromDatabase(key);
    }
    
    @CachePut(value = "shared-data", key = "#key")
    public String setData(String key, String value) {
        // Обновляем в БД
        saveToDatabase(key, value);
        return value;
    }
    
    private String fetchDataFromDatabase(String key) {
        // Реализация...
        return "data";
    }
    
    private void saveToDatabase(String key, String value) {
        // Реализация...
    }
}

// В application.yml
// spring:
//   cache:
//     type: caffeine
//     caffeine:
//       spec: maximumSize=500,expireAfterWrite=10m

7. Полный пример: REST контроллер с общими данными

import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.*;

@Component
public class SessionManager {
    private final ConcurrentHashMap<String, UserSession> sessions = new ConcurrentHashMap<>();
    
    public void createSession(String sessionId, String userId) {
        sessions.put(sessionId, new UserSession(userId, LocalDateTime.now()));
    }
    
    public UserSession getSession(String sessionId) {
        return sessions.get(sessionId);
    }
    
    public void invalidateSession(String sessionId) {
        sessions.remove(sessionId);
    }
    
    public int getActiveSessionCount() {
        return sessions.size();
    }
}

@RestController
@RequestMapping("/api/sessions")
public class SessionController {
    private final SessionManager sessionManager;
    
    public SessionController(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }
    
    @PostMapping
    public void createSession(@RequestParam String sessionId, @RequestParam String userId) {
        sessionManager.createSession(sessionId, userId);
    }
    
    @GetMapping("/{sessionId}")
    public UserSession getSession(@PathVariable String sessionId) {
        return sessionManager.getSession(sessionId);
    }
    
    @DeleteMapping("/{sessionId}")
    public void invalidateSession(@PathVariable String sessionId) {
        sessionManager.invalidateSession(sessionId);
    }
    
    @GetMapping("/count")
    public int getActiveCount() {
        return sessionManager.getActiveSessionCount();
    }
}

public class UserSession {
    private final String userId;
    private final LocalDateTime createdAt;
    
    public UserSession(String userId, LocalDateTime createdAt) {
        this.userId = userId;
        this.createdAt = createdAt;
    }
    
    // Getters...
}

Сравнение подходов

ПодходИспользованиеПотокобезопасностьМасштабируемость
Singleton BeanПростые случаиТребует синхронизацииОдин сервер
ConcurrentHashMapВ памяти, читаемо-тяжелоеВстроеннаяОдин сервер
ReadWriteLockМного читателейВстроеннаяОдин сервер
DatabaseПерсистентностьВстроеннаяМасштабируемо
RedisРаспределённое кэшированиеВстроеннаяМасштабируемо
SessionВеб-сессииВстроеннаяМасштабируемо

Рекомендации

  1. Используй ConcurrentHashMap для простых случаев
  2. Предпочитай Database для критичных данных
  3. Используй Redis для распределённого кэша
  4. Избегай synchronized — используй concurrent классы
  5. Тестируй многопоточность с помощью JUnit + Concurrency тестов

Потокобезопасность в тестах

import org.junit.jupiter.api.Test;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;

public class ThreadSafetyTest {
    
    @Test
    public void testConcurrentAccess() throws InterruptedException {
        SharedDataService service = new SharedDataService();
        ExecutorService executor = Executors.newFixedThreadPool(10);
        int iterations = 1000;
        
        for (int i = 0; i < iterations; i++) {
            final int value = i;
            executor.submit(() -> service.incrementCounter());
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        assertEquals(iterations, service.getCounter());
    }
}

Вывод: Используй ConcurrentHashMap + Singleton Bean для простого случая, Database для персистентности, и Redis для распределённых систем.

Как организовать доступ к общим данным в эндпоинте на Spring, чтобы они были разделяемыми между всеми клиентами и потоками | PrepBro