СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

Два простых примера создания файлового хранилища в СУБД в черновиках Из песочницы

Практически в каждом веб-проекте требуется собственное хранилище файлов. Назначений у него множество. Сегодня мы рассмотрим 2 простых варианта его создания: первый — с использованием типа данных blob средствами Java, Spring MVC, Hibernate, MySQL и второй — с кластеризацией (разбиением файла на кусочки) средствами groovy, grails, hibernate, PostgreSQL.

Зачем нужен этот велосипед? Зачастую нужно отдавать пользователю сформированные на стороне сервера файлы и предусмотреть возможность самому выкладывать туда что-нибудь. К тому же, мы работаем с СУБД, к которой можно подключиться по JDBC с других хостов, и если сделать реплицируемую базу с несколькими нодами, то получится хорошая балансировка нагрузки на скачивание.



Вариант 1 — Использование типа данных blob


Итак, у нас есть Spring MVC. Создаём бин с persistence слоя, в котором заложен функционал “объекта”, хранимого в базе

DataObject.java
package ru.cpro.uchteno.domain.attach;

import java.sql.Blob;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;

import org.hibernate.annotations.Type;

@Entity
@Table(name = "data_object")
public class DataObject {
    
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private Integer id;
    
    @Column(name="data_name")
    private String name;
    
    @Column(name="data_data", columnDefinition="longblob", length=2*1024*1024*1024)
    @Lob()
    private Blob data;
    
    @Column(name="contype")
    private String contentType;
    
    @Column(name="surname")
    private String surname;
    
    @Column(name="access_count")
    private Integer accessCount;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Blob getData() {
        return data;
    }

    public void setData(Blob data) {
        this.data = data;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public Integer getAccessCount() {
        return accessCount;
    }

    public void setAccessCount(Integer accessCount) {
        this.accessCount = accessCount;
    }
    
}



private Integer id — идентификатор, первичный ключ нашего объекта;
private String name — имя загруженного объекта;
private Blob data — данные объекта;
private String contentType — тип данных объекта;
private String surname — видимое пользователем имя объекта;
private Integer accessCount — количество скачиваний;

Теперь опишем интерфейс дао слоя для нашего объекта.

AttachmentDAO.java
package ru.cpro.uchteno.dao.attach;

import java.util.List;

import ru.cpro.uchteno.domain.attach.DataObject;

public interface AttachmentDAO {
    //Получить объект по айдишнику.
    public DataObject getObjectByID(Integer id);
    //Сохранить объект.
    public void saveOrUpdate(DataObject dao);
    //удалить объект.
    public void deleteDataObject(DataObject dao);
    //Получить список всех объектов.
    public List<DataObject> listObjects();
    //Получить объект по “фамилии”, видимому узеру имени.
    public DataObject getObjectBySurname(String surname);
    //Поиск объектов в базе по имени.
    public List<DataObject> searchObjectsByName(String query);
}



Делаем реализацию интерфейса:

AttachmentDAOImpl.java
package ru.cpro.uchteno.dao.attach;

import java.util.ArrayList;
import java.util.List;

import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.stereotype.Repository;

import ru.cpro.uchteno.domain.attach.DataObject;

@Repository
public class AttachmentDAOImpl implements AttachmentDAO {
	//Внедряем hibernateTemplate
	@Autowired
	private HibernateTemplate attachHibernateTemplate;

	@Override
	public List<DataObject> listObjects() {
		return attachHibernateTemplate.find("from DataObject");
	}

	@Override
	public DataObject getObjectByID(Integer id) {
		List<DataObject> doList = attachHibernateTemplate.find(
				"from DataObject where id = ?", id);
		if (doList == null || doList.size() <= 0)
			return null;
		return doList.get(0);
	}

	@Override
	public void saveOrUpdate(DataObject dao) {
		attachHibernateTemplate.saveOrUpdate(dao);
	}

	@Override
	public void deleteDataObject(DataObject dao) {
		attachHibernateTemplate.delete(dao);
	}

	@Override
	public DataObject getObjectBySurname(String surname) {
		List<DataObject> doList = attachHibernateTemplate.find(
				"from DataObject where surname = ?", surname);
		if (doList == null || doList.size() <= 0)
			return null;
		return doList.get(0);
	}

	@Override
	public List<DataObject> searchObjectsByName(String query) {
		if (query == null || query.trim().equals(""))
			return attachHibernateTemplate
					.find("from DataObject order by id desc limit 20");
		return attachHibernateTemplate.find(
				"from DataObject where name like ?", "%" + query + "%");
	}

}



В сервисе делегируем все методы из дао

AttachmentService.java
package ru.cpro.uchteno.service.attach;

import java.util.List;

import ru.cpro.uchteno.domain.attach.DataObject;

public interface AttachmentService {

    public DataObject getObjectByID(Integer id);
    
    public void saveOrUpdate(DataObject dao);
    
    public void deleteDataObject(DataObject dao);

    public List<DataObject> listObjects();

    public DataObject getObjectBySurname(String surname);
    
    public List<DataObject> searchObjectsByName(String query);
    
}



AttachmentServiceImpl.java
package ru.cpro.uchteno.service.attach;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.cpro.uchteno.dao.attach.AttachmentDAO;
import ru.cpro.uchteno.domain.attach.DataObject;

@Service
public class AttachmentServiceImpl implements AttachmentService {
    
    @Autowired
    private AttachmentDAO attachmentDAO;
    
    @Override
    public List<DataObject> listObjects() {
	return attachmentDAO.listObjects();
    }

    @Override
    public DataObject getObjectByID(Integer id) {
	return attachmentDAO.getObjectByID(id);
    }

    @Override
    public void saveOrUpdate(DataObject dao) {
	attachmentDAO.saveOrUpdate(dao);
    }

    @Override
    public void deleteDataObject(DataObject dao) {
	attachmentDAO.deleteDataObject(dao);
    }
    
    @Override
    public DataObject getObjectBySurname(String surname) {
	return attachmentDAO.getObjectBySurname(surname);
    }

    @Override
    public List<DataObject> searchObjectsByName(String query) {
	return attachmentDAO.searchObjectsByName(query);
    }

}



Всё, что нужно у нас есть. Теперь осталось только сделать контроллер:

AttachmentController.java
package ru.cpro.uchteno.web.attachment;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import ru.cpro.uchteno.domain.attach.DataObject;
import ru.cpro.uchteno.service.attach.AttachmentService;

@Controller()
@RequestMapping("/")
public class AttachmentController {
	//Подключаем наш сервис
	@Autowired
	private AttachmentService attachmentService;
	//Вывод на страницу всех файлов в базе
	@RequestMapping("/admin/dataObjects/private")
	public String index(HashMap<String, Object> map) {
		List<DataObject> doList = attachmentService.listObjects(); //список файлов
		map.put("dlist", doList);// заталкать его в кодель фтраницы
		return "admin/attach/attach";// вернуть путь к вьюхе
	}

	//Добавление файла
	@RequestMapping("/admin/dataObjects/private/add")
	public String add(
			HashMap<String, Object> map,
			@RequestParam(value = "addName", required = true) String name,
			@RequestParam(value = "addFile", required = true) MultipartFile file,
			@RequestParam(value = "addSurname", required = true) String surname) {
		try {
			DataObject dob = new DataObject();//Создать новый файл
			if (name == null || name.trim().equals("")) //Если имя файла пустое
				dob.setName(file.getOriginalFilename()); // Взять имя из  загружаемого файла
			else
				dob.setName(name); // иначе дать файлу новое имя
			Blob blob = Hibernate.createBlob(file.getInputStream()); //Создаем блоб по входному потоку
			dob.setData(blob);
			dob.setContentType(file.getContentType()); // Контент тайп
			dob.setSurname(surname); // задаем файлу “фамилию”
			attachmentService.saveOrUpdate(dob); // сохраняем файл
		} catch (IOException e) {
			System.out.println("Не удалось записать объект в базу");
			return "redirect:/admin/attach/dataObjects/private/";
		}
		return "redirect:/admin/dataObjects/private/";
	}

       //Глушим файлы по айдишнику
	@RequestMapping("/admin/dataObjects/private/del")
	public @ResponseBody
	String del(@RequestParam(value = "id", required = true) Integer id) {
		try {
			DataObject dao = attachmentService.getObjectByID(id);
			attachmentService.deleteDataObject(dao);
		} catch (Exception ex) {
			return "FAIL";
		}
		return "SUCCESS";
	}

//Скачивание файлов по фамилии. Публичная часть.
	@RequestMapping("/public/dataObjects/getObject")
	public void getObject(
			@RequestParam(value = "s", required = false) String surname,
			HttpServletResponse resp) {
		if (surname == null || surname.trim().equals("")) {
			try {
				resp.getOutputStream().close();
				return;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		DataObject dao = attachmentService.getObjectBySurname(surname); // Ищем файл по фамилии
		if (dao == null) {
			try {
				resp.getOutputStream().close();
			} catch (Exception ex) {
			}
			return;
		}
		// наращиваем счетчик скачиваний
		if (dao.getAccessCount() == null)
			dao.setAccessCount(1); 
		else
			dao.setAccessCount(dao.getAccessCount() + 1);
		attachmentService.saveOrUpdate(dao);

		//Отдаем юзеру файл
		Blob blob = dao.getData();
		try {
			InputStream is = blob.getBinaryStream();
			OutputStream os = resp.getOutputStream();
			resp.setContentType(dao.getContentType());
			int n = 0;
			byte buff[] = new byte[1024];
			while (n >= 0) {
				n = is.read(buff);
				if (n > 0)
					os.write(buff, 0, n);
			}
			is.close();
			os.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}



Вариант 2 — Файловое хранилище с кластеризацией


Я решил использовать grails. Но когда пишем на groovy, Blob не работает адекватно. Поэтому разобьем наш файл на кластеры и сохраним по-отдельности.

В принципе тут всё делается практически так же, как и в предыдущем примере. Приложение доступно здесь.

Спасибо всем за внимание! Надеюсь, что мои примеры будут вам полезны.

комментарии (10)

+3
kamaikin ,  

А сколько запросов файла в секунду выдержит такое хранилище?

+1
+3 –2
ostapbender ,  

Кровавый ынтырпрайз.

+1
AndersonDunai ,  

1) А чем этот метод «простой»? Кажется, это — обычное решение «в лоб».
2) Судя по соотношению кол-ва листинга и описания, вам на pastebin.

+5
Agb ,  

Хранить файлы (бинарный контент) в базе это модно? Думал всегда что файлы следует хранить на фс, а в базе линки.

–3
RomanPyr ,  

Попробую в продакшане…

+7
evalexdy ,  

Простите, но это бред какой-то. Поясню на примере:
Несколько лет назад наша тогдашняя команда делала сайт, с которого качались файлы, нагрузка была примерно такая: около 15000 посетителей в день, скачивание около 2.5Tb в день. С учетом того, что у заказчика был ограниченный бюджет на железо, наиболее эффективной (с точки зрения нагрузки, денег и отказоустойчивости) оказалась следующая конфигурация («сервера» в кавычках, поскольку это были обычные десктопы):
1 основной «сервер», на который шли запросы от пользователей и, собственно, крутился сайт;
2 дополнительных «файл-сервера»;
6 файловых хранилищ офисного типа, в каждом хранилище 6 винтов по терабайту каждый, в RAID, хранилища были подключены по 3 штуки к каждому «файл-серверу».

Схема была такая, пользователь сидит на сайте, общается и вдруг желает скачать файл. Поскольку доступ к сайту был платным, каждый кусочек файла перед скачиванием обращался к PHP скрипту, который, в свою очередь лазил в базу, проверял оплату и еще ряд параметров. Обращение к скрипту было сделано средствами nginx (технических деталей по nginx'у не знаю, я программист, а не админ), что позволило всю раздачу вести именно через nginx. Основной сервер определял по базе на каком хранилище лежит нужный пользователю файл и передавал скачивание туда через нужный «файл-сервер».

Если пользователь загружал файл (по FTP или через специальный загрузчик), то файл сначала загружался на ближайшее файловое хранилище, а уже потом, после проверки модератором, к нему формировались ссылки в базе данных и он включался в общую систему раздачи.

Перепробовали мы очень много вариантов, этот оказался самым лучшим, с учетом того «оборудования» с которым нам пришлось иметь дело. Но в любом случае, все наши варианты рождали приличную нагрузку к базе на одних только проверках, при попытке размещать файлы в blob'ах — тогдашний «кластер» из «серверов» — падал со свистом уже на первом десятке одновременных скачиваний.

+1
AmdY ,  

Dы забыли ключевой момент — x-accel-redirect. Без него и городят костыли со скриптами для отдачи и хранением в бд.

0
evalexdy ,  

Ну почему же, вот сходу цитата из исходников того проекта:

header( 'Content-Length: ' . $size );
header( 'Content-Range: bytes '.$range_from.'-'.$range_to.'/'.$fsize );
header( 'X-Accel-Redirect: /files/'.$files_folder_new.$innerpath );

Я вообще-то идею описывал, а не детальную реализацию.
0
AmdY ,  

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

0
kamaikin ,  

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