В дополнение к использованию промежуточного программного обеспечения для распределения запросов через прокси, другим распространенным методом для подбазы данных и подтаблицы базы данных является подбаза данных и подтаблица на уровне клиента — путем надлежащей упаковки клиентского кода операция доступа к базе данных подбазы данных и подтаблица стала возможной.Код тоже очень удобно писать. Решение подтаблицы подбазы данных в этой статье основано на платформе MyBatis, но отличается от широко используемых решений на рынке.Они обычно переписывают операторы SQL, создавая сложные подключаемые модули MyBatis.Такой код подключаемого модуля будет быть чрезвычайно сложными и в конечном итоге могут быть только плагинами.Первоначальный автор проекта может полностью понять соответствующий код, что создает определенные проблемы с ремонтопригодностью проекта. Решение в этой статье очень простое и понятное, а также удобное в использовании. Его философия дизайна исходит из Python - явный лучше, чем неявный, то есть явный лучше, чем неявный, и он не будет скрывать процесс подтаблицы подбазы данных.
Многие конструкции подбаз данных и подтаблиц пытаются скрыть логику подбаз данных и подтаблиц в реализации, но в этом нет необходимости. Пользователь должен знать, что подтаблица подбазы данных фактически выполняется за кулисами, иначе как он не сможет выполнить поиск по глобальному индексу? Как он мог не выполнять операции соединения нескольких таблиц по своему желанию. Если вы действительно используете его как единую таблицу, при выходе в онлайн будут большие проблемы.
Название проекта: шардино, адрес проекта:GitHub.com/friends/what…
Далее давайте посмотрим на форму кода операции с базой данных по схеме из этой статьи.
Всего в таблице сообщений 64 таблицы, и в одну из таблиц будут распределяться разные записи либо по хешу, либо по дате, логика распределения определяется кодом пользователя. Количество подтаблиц может быть установлено на разные значения в разных средах.Например, при модульном тестировании количество подтаблиц установлено на 4, а в режиме онлайн может потребоваться установить на 64.
@Configuration
public class PartitionConfig {
private int post = 64;
public int post() {
return post;
}
public void post(int post) {
this.post = post;
}
}
Таблица сообщений будет снова распределена по нескольким библиотекам, и распределение по модулю выполняется непосредственно здесь. Предполагая, что существует 4 базы данных сообщений, таблица сообщений делится на 64 таблицы, а именно: post_0, post_1, post_2 и post_63. Затем post_0, post_4, post_8 и т. д. выделяются в библиотеку № 0, post_1, post_5, post_9 и т. д. выделяются в библиотеку № 1, post_2, post_6, post_10 и т. д. выделяются в библиотеку № 2, post_3 , post_5, post_11 и т.д. размещены в библиотеке №4.
Создайте групповой объект базы данных MySQLGroupStore из файла конфигурации.Этот объект является точкой входа для выполнения операций MySQL, с помощью которых мы можем найти определенные физические источники данных MySQL master-slave.
@Configuration
public class RepoConfig {
@Autowired
private Environment env;
private MySQLGroupBuilder mysqlGroupBuilder = new MySQLGroupBuilder();
@Bean
@Qualifier("post")
public MySQLGroupStore replyMySQLGroupStore() {
MySQLGroupStore store = mysqlGroupBuilder.buildStore(env, "post");
store.prepare(factory -> {
factory.getConfiguration().addMapper(PostMapper.class);
});
return store;
}
}
Конфигурационный файл application.properties выглядит следующим образом
mysql.post0.master.addrWeights=localhost:3306
mysql.post0.master.db=sample
mysql.post0.master.user=sample
mysql.post0.master.password=123456
mysql.post0.master.poolSize=10
mysql.post0.slave.addrWeights=localhost:3307=100&localhost:3308=100
mysql.post0.slave.db=sample
mysql.post0.slave.user=sample
mysql.post0.slave.password=123456
mysql.post0.slave.poolSize=10
mysql.post1.master.addrWeights=localhost:3309
mysql.post1.master.db=sample
mysql.post1.master.user=sample
mysql.post1.master.password=123456
mysql.post1.master.poolSize=10
mysql.post1.slave.addrWeights=localhost:3310=100&localhost:3311=100
mysql.post1.slave.db=sample
mysql.post1.slave.user=sample
mysql.post1.slave.password=123456
mysql.post1.slave.poolSize=10
mysqlgroup.post.nodes=post0,post1
mysqlgroup.post.slaveEnabled=true
Группа базы данных здесь состоит из нескольких одноранговых пар Master-Slave.Каждый Master-Slave состоит из главной библиотеки и нескольких подчиненных библиотек с разным весом.Количество пар Master-Slave равно количеству подбиблиотек. .
mysqlgroup также имеет специальную опцию конфигурации slaveEnabled, чтобы контролировать, требуется ли подчиненной библиотеке закрыть разделение чтения-записи, которое закрыто по умолчанию, поэтому объекты, связанные с экземпляром подчиненной библиотеки, не будут создаваться.
post_k Суффикс k этой таблицы называется номером раздела, который представляет собой переменную раздела, используемую повсюду в последующем коде, указывающую, что текущая запись присваивается порядковому номеру соответствующей таблицы физических данных. Нам нужно вычислить номер раздела в соответствии с содержимым записи, а затем определить, к какой физической базе данных относится физическая таблица, к которой принадлежит эта запись, в соответствии с номером раздела, а затем выполнить соответствующие операции чтения и записи в этой физической базе данных.
В этом примере таблица постов хеширует 64 таблицы в соответствии с полем userId, которые равномерно распределены по 2 парам физических библиотек, каждая из которых содержит главную библиотеку и 2 подчиненные библиотеки.
С экземпляром MySQLGroupStore мы можем манипулировать всеми базами данных сколько душе угодно.
@Repository
public class PostMySQL {
@Autowired
private PartitionConfig partitions;
@Autowired
@Qualifier("post")
private MySQLGroupStore mysql;
public void createTables() {
for (int i = 0; i < partitions.post(); i++) {
int k = i;
mysql.master(k).execute(session -> {
PostMapper mapper = session.getMapper(PostMapper.class);
mapper.createTable(k);
});
}
}
public void dropTables() {
for (int i = 0; i < partitions.post(); i++) {
int k = i;
mysql.master(k).execute(session -> {
PostMapper mapper = session.getMapper(PostMapper.class);
mapper.dropTable(k);
});
}
}
public Post getPostFromMaster(String userId, String id) {
Holder<Post> holder = new Holder<>();
int partition = this.partitionFor(userId);
mysql.master(partition).execute(session -> {
PostMapper mapper = session.getMapper(PostMapper.class);
holder.value(mapper.getPost(partition, id));
});
return holder.value();
}
public Post getPostFromSlave(String userId, String id) {
Holder<Post> holder = new Holder<>();
int partition = this.partitionFor(userId);
mysql.slave(partition).execute(session -> {
PostMapper mapper = session.getMapper(PostMapper.class);
holder.value(mapper.getPost(partition, id));
});
return holder.value();
}
public void savePost(Post post) {
int partition = this.partitionFor(post);
mysql.master(partition).execute(session -> {
PostMapper mapper = session.getMapper(PostMapper.class);
Post curPost = mapper.getPost(partition, post.getId());
if (curPost != null) {
mapper.updatePost(partition, post);
} else {
mapper.insertPost(partition, post);
}
});
}
public void deletePost(String userId, String id) {
int partition = this.partitionFor(userId);
mysql.master(partition).execute(session -> {
PostMapper mapper = session.getMapper(PostMapper.class);
mapper.deletePost(partition, id);
});
}
private int partitionFor(Post post) {
return Post.partitionFor(post.getUserId(), partitions.post());
}
private int partitionFor(String userId) {
return Post.partitionFor(userId, partitions.post());
}
}
Из приведенного выше кода видно, что первым шагом всех операций чтения, записи, создания и удаления таблиц является вычисление номера раздела, а затем выбор целевой библиотеки master-slave на его основе, а затем дальнейшая работа с целевой Таблица данных. Здесь у меня по умолчанию включена автоматическая фиксация, поэтому нет необходимости явно использовать session.commit() .
mysql.master(partition)
mysql.slave(partition)
// 如果没有分库
mysql.master()
mysql.slave()
// 如果既没有分库也没有读写分离
mysql.db()
// 操作具体的表时要带 partition
mapper.getPost(partition, postId)
mapper.savePost(partition, post)
Во время работы с таблицей данных необходимо передать конкретный номер раздела, чтобы MyBatis мог знать, какая таблица разделов используется.
public interface PostMapper {
@Update("create table if not exists post_#{partition}(id varchar(128) primary key not null, user_id varchar(1024) not null, title varchar(1024) not null, content text, create_time timestamp not null) engine=innodb")
public void createTable(int partition);
@Update("drop table if exists post_#{partition}")
public void dropTable(int partition);
@Results({@Result(property = "createTime", column = "create_time"),
@Result(property = "userId", column = "user_id")})
@Select("select id, user_id, title, content, create_time from post_#{partition} where id=#{id}")
public Post getPost(@Param("partition") int partition, @Param("id") String id);
@Insert("insert into post_#{partition}(id, user_id, title, content, create_time) values(#{p.id}, ${p.userId}, #{p.title}, #{p.content}, #{p.createTime})")
public void insertPost(@Param("partition") int partition, @Param("p") Post post);
@Update("update post_#{partition} set title=#{p.title}, content=#{p.content}, create_time=#{p.createTime} where id=#{p.id}")
public void updatePost(@Param("partition") int partition, @Param("p") Post post);
@Delete("delete from post_#{partition} where id=#{id}")
public void deletePost(@Param("partition") int partition, @Param("id") String id);
}
Вы должны включать параметр раздела в каждую операцию с базой данных, что может показаться вам немного громоздким. Но это также очень интуитивно понятно, оно четко говорит нам, какая конкретная подтаблица работает в данный момент.
В классе Mapper аннотации MyBatis, если метод содержит несколько параметров, аннотацию @Param необходимо использовать для аннотации имени, чтобы соответствующее имя аннотации можно было использовать непосредственно в операторе SQL. В противном случае вам придется использовать имена заполнителей переменных по умолчанию param0, param1 для представления, что очень неинтуитивно.
Пишем алгоритм хеширования подтаблицы в классе сущностей Post, где для хеширования используется алгоритм CRC32.
public class Post {
private String id;
private String userId;
private String title;
private String content;
private Date createTime;
public Post() {}
public Post(String id, String userId, String title, String content, Date createTime) {
this.id = id;
this.userId = userId;
this.title = title;
this.content = content;
this.createTime = createTime;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public int partitionFor(int num) {
return partitionFor(userId, num);
}
public static int partitionFor(String userId, int num) {
CRC32 crc = new CRC32();
crc.update(userId.getBytes(Charsets.UTF8));
return (int) (Math.abs(crc.getValue()) % num);
}
}
Параметр num метода partitionFor в коде — это общее количество разделяемых таблиц. Если таблица разбита по дате, этот параметр может не понадобиться, достаточно вернуть целое число даты, например 20190304.
Остается последний вопрос о том, как вероятностно назначаются несколько взвешенных подчиненных библиотек. Здесь мы будем использовать AbstractRoutingDataSource, который поставляется с spring-jdbc — источником данных с функцией маршрутизации. Он может содержать несколько источников подданных, а затем динамически выбирать источник данных в соответствии с определенным алгоритмом стратегии, здесь используется случайный вес.
Но есть проблема, мне нужен только этот класс здесь, но мне нужно ввести весь пакет spring-boot-jdbc-starter, который немного запутан. Я изучил код класса AbstractRoutingDataSource и обнаружил, что его реализация очень проста, если я имитирую его и реализую простую версию, то не нужно будет вводить весь код пакета.
public class RandomWeightedDataSource extends DataSourceAdapter {
private int totalWeight;
private Set<PooledDataSource> sources;
private Map<Integer, PooledDataSource> sourceMap;
public RandomWeightedDataSource(Map<PooledDataSource, Integer> srcs) {
this.sources = new HashSet<>();
this.sourceMap = new HashMap<>();
for (Entry<PooledDataSource, Integer> entry : srcs.entrySet()) {
// 权重值不宜过大
int weight = Math.min(10000, entry.getValue());
for (int i = 0; i < weight; i++) {
sourceMap.put(totalWeight, entry.getKey());
totalWeight++;
}
this.sources.add(entry.getKey());
}
}
private PooledDataSource getDataSource() {
return this.sourceMap.get(ThreadLocalRandom.current().nextInt(totalWeight));
}
public void close() {
for (PooledDataSource ds : sources) {
ds.forceCloseAll();
}
}
@Override
public Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return getDataSource().getConnection(username, password);
}
}
Если вам нужно глубже понять код его реализации, вы можете вытащить репозиторий кода шардино на локальный сервер и запустить его.
git clone https://github.com/pyloque/shardino.git
Есть юнит-тесты, которые можно запускать, перед запуском нужно убедиться, что на машине установлена среда docker.
docker-compose up -d
Эта команда запустит 2 пары библиотек master-slave, каждая с одним ведущим и двумя подчиненными.
Хотя в этом примере используется Springboot, он использует только свои удобные функции внедрения зависимостей и модульного тестирования, а Shardino может существовать независимо от Springboot.
shardino не является идеальной библиотекой с открытым исходным кодом, это просто шаблон для реализации кода.Если читатели используют другие базы данных или другие версии MySQL, им необходимо настроить код для адаптации.