Обслуживание больших объектов PostgreSQL через HTTP - PullRequest
0 голосов
/ 05 октября 2018

Я создаю приложение для обслуживания данных из базы данных PostgreSQL через REST API (с Spring MVC) и PWA (с Vaadin).

В базе данных PostgreSQL хранятся файлы размером до 2 ГБ с использованием Большие объекты (я не контролирую это);драйвер JDBC обеспечивает потоковый доступ к их двоичному содержимому через Blob#getBinaryStream, поэтому данные не должны считываться целиком в память.

Единственное требование состоит в том, что поток из большого двоичного объекта должениспользовать в той же транзакции, в противном случае драйвер JDBC сгенерирует.

Проблема в том, что даже если я получу поток в методе транзакционного репозитория, Spring MVC и Vaadin StreamResource будут использовать его вне транзакциипоэтому драйвер JDBC выбрасывает.

Например, если

public interface SomeRepository extends JpaRepository<SomeEntity, Long> {

    @Transactional(readOnly = true)
    default InputStream getStream() {
        return findById(1).getBlob().getBinaryStream();
    }
}

, этот метод Spring MVC завершится с ошибкой

@RestController
public class SomeController {

    private final SomeRepository repository;

    @GetMapping
    public ResponseEntity getStream() {
        var stream = repository.getStream();
        var resource = new InputStreamResource(stream);
        return new ResponseEntity(resource, HttpStatus.OK);
    }
}

, и то же самое для этого Vaadin StreamResource

public class SomeView extends VerticalLayout {

    public SomeView(SomeRepository repository) {
        var resource = new StreamResource("x", repository::getStream);
        var anchor = new Anchor(resource, "Download");
        add(anchor);
    }
}

с тем же исключением:

org.postgresql.util.PSQLException: ERROR: invalid large-object descriptor: 0

, что означает, что транзакция уже закрыта при чтении потока.

Я вижу два возможных решения этой проблемы:

  1. держать транзакцию открытой во время загрузки;
  2. записывать поток на диск во время транзакции и затем передавать файл с диска во время загрузки.

Решение 1 является антишаблоном и угрозой безопасности: длительность транзакции остается на руках клиента, и медленный читатель или злоумышленник могут заблокировать доступ к данным.

Решение 2 создаетогромная задержка между запросом клиента и ответом сервера, так как поток сначала читается из базы данных и записывается на диск.

Одной из идей может быть начало чтения с диска, пока файл записывается с данными.из базы данных, так что передача начинается немедленно, но длительность транзакции будет отделена от загрузки клиента;но я не знаю, какие побочные эффекты это может иметь.

Как мне достичь цели обслуживания больших объектов PostgreSQL безопасным и производительным способом?

Ответы [ 2 ]

0 голосов
/ 30 октября 2018

Мы решили эту проблему в Spring Content , используя потоки + конвейерные потоки и специальную оболочку входного потока ClosingInputStream, которая задерживает закрытие соединения / транзакции, пока потребитель не закроет входной поток.Может быть, что-то вроде это тоже вам поможет?

Просто как к сведению.Мы обнаружили, что использование OIDs Postgres и API-интерфейса Large Object очень медленное по сравнению с аналогичными базами данных.

Возможно, также возможно, что вы сможете просто модифицировать Spring Content JPA для своего решения и, следовательно, использовать его конечные точки http (и решение, которое я только что обрисовал в общих чертах) вместо создания своего собственного?Примерно так: -

pom.xml

   <!-- Java API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-jpa-boot-starter</artifactId>
      <version>0.4.0</version>
   </dependency>

   <!-- REST API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-rest-boot-starter</artifactId>
      <version>0.4.0</version>
   </dependency>

SomeEntity.java

@Entity
public class SomeEntity {
   @Id
   @GeneratedValue
   private long id;

   @ContentId
   private String contentId;

   @ContentLength
   private long contentLength = 0L;

   @MimeType
   private String mimeType = "text/plain";

   ...
}

SomeEntityContentStore.java

@StoreRestResource(path="someEntityContent")
public interface SomeEntityContentStore extends ContentStore<SomeEntity, String> {
}

Это все, что вам нужно для получения конечных точек REST, которые позволят вам связать контент с вашей сущностью SomeEntity.В наших примерах есть рабочий пример здесь .

0 голосов
/ 05 октября 2018

Один из вариантов - отключить чтение из базы данных и запись ответа клиенту, как вы упомянули.Недостатком является сложность решения, вам нужно будет синхронизировать между читателем и писателем.

Другой вариант - сначала получить большой идентификатор объекта в основной транзакции, а затем прочитать данные в виде фрагментов, каждый блокв отдельной транзакции.

byte[] getBlobChunk(Connection connection, long lobId, long start, long chunkSize) throws SQLException { 
   Blob blob = PgBlob(connection, lobId);
   InputStream is = blob.getBinaryStream(start, chunkSize);
   return IOUtils.toByteArray(is);
}

Это решение намного проще, но имеет дополнительные издержки на установление нового соединения, которое не должно иметь большого значения, если вы используете пул соединений.

...