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

Как хранить состояние в сервисе

2.0 Middle🔥 181 комментариев
#REST API и микросервисы

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

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

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

Как хранить состояние в сервисе

Это критически важный вопрос архитектуры. Состояние — это данные, которые меняются и должны быть доступны между запросами. Неправильное хранение состояния — основная причина багов в распределённых системах.

Принцип: Stateless Services

В микросервисной архитектуре сервис должен быть stateless (без состояния). Это означает:

  • Каждый запрос независим
  • Сервис не хранит данные между запросами
  • Любой экземпляр сервиса может обработать любой запрос

Это позволяет:

  • Масштабировать горизонтально (добавлять экземпляры)
  • Лучше обрабатывать отказы
  • Легче разворачивать обновления

Где хранить состояние?

1. Base case — не хранить в памяти сервиса!

ЭТО ПЛОХО:

@Service
public class BadUserService {
    private Map<UUID, User> usersCache = new HashMap<>(); // ОПАСНО!
    
    public User getUser(UUID id) {
        // Если сервис перезагрузится — все потеряется
        return usersCache.getOrDefault(id, null);
    }
    
    public void updateUser(UUID id, User user) {
        usersCache.put(id, user); // Не синхронизирована с БД
    }
}

Проблемы:

  • При перезагрузке сервиса состояние теряется
  • При горизонтальном масштабировании разные экземпляры имеют разные состояния
  • Синхронизация состояния между экземплярами невозможна

2. Правильно — хранить в БД

ЭТО ХОРОШО:

@Service
public class UserService {
    private final UserRepository userRepository; // БД — источник истины
    
    public User getUser(UUID id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }
    
    public User updateUser(UUID id, UpdateUserRequest request) {
        User user = getUser(id); // Читаем из БД
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        return userRepository.save(user); // Сохраняем в БД
    }
}

Преимущества:

  • Состояние сохраняется при перезагрузке
  • Все экземпляры сервиса видят одно и то же состояние
  • Масштабируется горизонтально

3. Session State (состояние сессии)

Для веб-приложений нужно хранить состояние пользователя между запросами.

ПЛОХА:

// Сессия в памяти
@Service
public class SessionManager {
    private Map<String, SessionData> sessions = new HashMap<>();
    
    public void createSession(String sessionId, SessionData data) {
        sessions.put(sessionId, data); // Потеряется при перезагрузке
    }
}

ХОРОШО — использовать Redis:

@Service
public class SessionService {
    private final RedisTemplate<String, SessionData> redisTemplate;
    private static final Duration SESSION_TIMEOUT = Duration.ofHours(1);
    
    public void createSession(String sessionId, SessionData data) {
        String key = "session:" + sessionId;
        redisTemplate.opsForValue().set(key, data, SESSION_TIMEOUT);
    }
    
    public SessionData getSession(String sessionId) {
        String key = "session:" + sessionId;
        return (SessionData) redisTemplate.opsForValue().get(key);
    }
}

Или с JWT токенами (лучший вариант):

@Service
public class JwtAuthService {
    private final JwtTokenProvider tokenProvider;
    
    public String generateToken(User user) {
        // Все состояние в самом токене (закодировано и подписано)
        return tokenProvider.generateToken(user);
    }
    
    public User validateAndGetUser(String token) {
        // Декодируем токен, получаем данные
        return tokenProvider.getUserFromToken(token);
    }
}

Кэширование состояния

Иногда нужно кэшировать состояние для производительности, но правильно.

1. Кэш с инвалидацией

@Service
@EnableCaching
public class ProductService {
    private final ProductRepository productRepository;
    
    @Cacheable(value = "products", key = "#id")
    public Product getProduct(UUID id) {
        // Первый раз читаем из БД и кэшируем
        return productRepository.findById(id).orElseThrow();
    }
    
    @CacheEvict(value = "products", key = "#id")
    public Product updateProduct(UUID id, UpdateProductRequest request) {
        Product product = getProduct(id);
        product.setName(request.getName());
        Product updated = productRepository.save(product);
        // Кэш автоматически инвалидируется
        return updated;
    }
    
    @CacheEvict(value = "products", allEntries = true)
    public void clearAllCache() {
        // Очищаем весь кэш если нужно
    }
}

2. Распределённый кэш (Redis)

@Service
public class UserCacheService {
    private final UserRepository userRepository;
    private final RedisTemplate<String, User> redisTemplate;
    private static final Duration CACHE_TTL = Duration.ofMinutes(30);
    
    public User getUser(UUID id) {
        String key = "user:" + id;
        
        // Пытаемся получить из кэша
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        
        // Если нет в кэше, читаем из БД
        user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
        
        // Сохраняем в кэш
        redisTemplate.opsForValue().set(key, user, CACHE_TTL);
        return user;
    }
    
    public User updateUser(UUID id, UpdateUserRequest request) {
        User user = getUser(id); // Может быть из кэша
        user.setName(request.getName());
        User updated = userRepository.save(user);
        
        // Инвалидируем кэш
        String key = "user:" + id;
        redisTemplate.delete(key);
        
        return updated;
    }
}

State Machines (конечные автоматы)

Для процессов с состояниями используйте State Machine, но храните состояние в БД:

// Статус заказа
public enum OrderStatus {
    NEW, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status = OrderStatus.NEW;
    
    @Version // Оптимистичная блокировка
    private Long version;
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    
    @Transactional
    public void processOrder(UUID orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        
        // Проверяем текущее состояние
        if (order.getStatus() != OrderStatus.NEW) {
            throw new InvalidOrderStateException("Cannot process order in status: " + order.getStatus());
        }
        
        // Меняем состояние
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order); // Сохраняем в БД
    }
    
    @Transactional
    public void shipOrder(UUID orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        
        if (order.getStatus() != OrderStatus.PROCESSING) {
            throw new InvalidOrderStateException("Can only ship orders in PROCESSING status");
        }
        
        order.setStatus(OrderStatus.SHIPPED);
        orderRepository.save(order);
    }
}

User Session State (для веб-приложений)

Spring Session с Redis

@Configuration
@EnableSpringHttpSession
public class HttpSessionConfig {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}

// Использование
@RestController
public class UserController {
    @PostMapping("/login")
    public void login(@RequestBody LoginRequest request, HttpSession session) {
        User user = authenticate(request);
        session.setAttribute("userId", user.getId()); // Сохраняется в Redis
    }
    
    @GetMapping("/me")
    public UserDTO getCurrentUser(HttpSession session) {
        UUID userId = (UUID) session.getAttribute("userId"); // Читается из Redis
        return new UserDTO(userService.getUser(userId));
    }
}

Асинхронное состояние (Long-running processes)

Для длительных операций используйте Job/Task table:

@Entity
@Table(name = "async_jobs")
public class AsyncJob {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @Enumerated(EnumType.STRING)
    private JobStatus status = JobStatus.PENDING; // NEW, RUNNING, COMPLETED, FAILED
    
    private String jobType;
    private String payload; // JSON with job data
    private String result; // JSON with result
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

@Service
public class AsyncJobService {
    private final AsyncJobRepository jobRepository;
    
    public UUID startJob(String jobType, String payload) {
        AsyncJob job = new AsyncJob();
        job.setJobType(jobType);
        job.setPayload(payload);
        job.setStatus(JobStatus.PENDING);
        AsyncJob saved = jobRepository.save(job);
        
        // Отправляем в очередь обработки (Kafka, RabbitMQ)
        jobQueue.send(new JobMessage(saved.getId(), jobType, payload));
        
        return saved.getId();
    }
    
    public AsyncJob getJobStatus(UUID jobId) {
        return jobRepository.findById(jobId)
            .orElseThrow(() -> new JobNotFoundException(jobId));
    }
}

Правила хранения состояния

  1. Основное правило: Состояние в БД, не в памяти

  2. Кэширование: Кэшируйте для производительности, но с инвалидацией

  3. Распределённое состояние: Используйте Redis для сессий и кэша

  4. Транзакции: Используйте @Transactional для атомарности

  5. Оптимистичная блокировка: Используйте @Version для конкурентного доступа

  6. Асинхронные процессы: Храните статус в БД

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

Тип состоянияГде хранитьИнструментTimeout
Пользовательские данныеБДPostgreSQL, MySQLN/A
КэшРаспределённый кэшRedis, Memcached5-30 мин
СессияРаспределённый кэшRedis, Session Store30 мин - 1 день
ТокенКлиент (JWT)JWT1 час - 1 день
Очередь задачБД + очередьKafka, RabbitMQДо обработки
Кэш BDDIn-memory + инвалидацияSpring CacheПо необходимости

Заключение

Основной принцип: сервис должен быть stateless, состояние хранится в БД или Redis. Это обеспечивает масштабируемость, надёжность и простоту отладки распределённых систем.

Как хранить состояние в сервисе | PrepBro