Данные базы данных в недавно запущенном проекте приближаются к насыщению. Самые большие данные о таблице составляют 3000 Вт, и есть несколько таблиц с миллионами данных. Проект требует, чтобы время считывания данных не могло превышать 0,05 секунды, но фактическая ситуация не соответствует требованиям. Объясните индексацию, и использование технологии Redis и Ehcache Cache больше не может соответствовать требованиям. Поэтому мы начали использовать технологию разделения чтения и записи. Возможно, когда в будущем объем данных превышает 100 миллионов или более, нам необходимо рассмотреть развертывание распределенных баз данных. Однако в настоящее время разделение чтения и записи + кэш + индекс + раздел таблицы + оптимизация SQL + балансировка нагрузки может соответствовать работе по запросу 100 миллионов объемов данных. Давайте посмотрим на шаги, чтобы использовать пружину для достижения разделения чтения и записи:
1. Фон
Наше общее приложение состоит в том, чтобы «читать больше и писать меньше» для баз данных, что означает, что давление на базу данных для чтения данных относительно высокое. Одна идея состоит в том, чтобы использовать решение для кластера базы данных.
Одним из них является основная библиотека, которая отвечает за написание данных, которые мы называем: написание библиотеки;
Все остальные из библиотеки, которая отвечает за чтение данных, которые мы называем: чтение библиотеки;
Итак, требования для нас:
1. Данные библиотеки чтения и библиотеки записи последовательны; (Это очень важная проблема. Обработка бизнес -логики должна быть обработана на уровне обслуживания, а не на уровне DAO или Mapper)
2. При написании данных вы должны написать их в библиотеку письма;
3. Вы должны перейти в библиотеку чтения, чтобы прочитать данные;
2. План
Существует два решения для решения разделения чтения и записи: решение для приложений и решение промежуточного программного обеспечения.
2.1. Решение уровня приложения:
преимущество:
1. Несколько источников данных легко переключаться и автоматически завершены программой;
2. Промежуточное программное обеспечение не требуется;
3. Теоретически поддерживайте любую базу данных;
недостаток:
1. Завершены программистами, а эксплуатация и техническое обслуживание не участвуют;
2. Динамически увеличить источники данных не может быть достигнуто;
2.2. Решение промежуточного программного обеспечения
Плюсы и минусы:
преимущество:
1. Исходная программа может достичь разделения чтения и записи без каких -либо изменений;
2. Динамическое добавление источников данных не требует перезапуска программы;
недостаток:
1. Программы полагаются на промежуточное программное обеспечение, что затрудняет переключение баз данных;
2. Промежуточное программное обеспечение используется в качестве транзитного агента, и производительность снизилась;
3. Используйте Spring для реализации на основе уровня приложений
3.1. Принцип
Перед входом в службу используйте AOP, чтобы вынести решение, будь то библиотека записи или библиотеку чтения, основание суждения может быть оценена на основе имени метода, например, с запроса, поиска, получения и т. Д. И другой библиотеки записи.
3.2. DynamicDataSource
Import org.springframework.jdbc.datasource.lookup.abstractroutingDatasource;/*** Определить динамические источники данных и реализовать AbstractroutingDatasource, предоставленные интеграционной пружиной. Вам нужно только реализовать метод DegineCurrentLookupkey * *, поскольку DynamicDataSource-это синглтон и нехватка потока, Threadlocal используется для обеспечения безопасности потока, которая завершается DynamicDatasourceHolder. * * @author zhijun * */public class dynamicdatasource extractroutingdatasource {@override защищенный объект deginecurrentlookupkey () {// Использовать DynamicDataSourceholder для обеспечения безопасности потока и получить источник данных в текущей теме DynamicDatasoursholder.getDataSourcekey (); }} 3.3. DynamicDatasourceholder
/** * * Используйте Threadlocal Technology для записи ключа источника данных в текущем потоке * * @author Zhijun * */public Class DynamicDatasourceholder {// Написать источник данных, соответствующий библиотеке частной статической конечной строки Master = "Master"; // Читать источник данных, соответствующий библиотеке частной статической статической конечной строки slav = "slave"; // Использование Threadlocal для записи источника данных текущего потока Private Static Final Threadlocal <string> holder = new Threadlocal <string> (); / ** * Установите клавишу источника данных * @param клавиши */ public static void putdatasourcekey (string key) {holder.set (key); } / ** * Получить клавишу источника данных * @return * / public Static String getDataSourceKey () {return holder.get (); } / *** Библиотека записи разметки* / public static void markmaster () {putdatasourcekey (Master); } / *** Markup Read Library* / public static void markslave () {putdatasourcekey (Slave); }} 3.4. DataSourceSpect
Import org.apache.commons.lang3.stringutils; import org.aspectj.lang.joinpoint;/** * Определите раздел AOP источника данных и судите, пора ли прочитать библиотеку или написать библиотеку через имя метода * @author Zhijun * * * void до (joinpoint point) {// Получить в данный момент выполненный метод string string methodname = point.getSignature (). getName (); if (isslave (methodname)) {// mark как библиотека чтения DynamicDatasourceholder.marksLave (); } else {// отмечать как библиотека записи DynamicDatasourceholder.markmaster (); }} / ** * Определите, является ли это библиотекой чтения * * @param methodname * @return * / private boolean isslave (string methodname) {// Имя метода начинается с запроса, найти, получить, вернуть stringutils.startswithany (методн название, «запрос», «найти», «получить»); }}3.5. Настройка 2 источника данных
3.5.1. JDBC.Properties
jdbc.master.driver = com.mysql.jdbc.driverjdbc.master.url = jdbc: mysql: //127.0.0.1: 3306/mybatis_1128? useunicode = true & ch aracterencoding = utf8 & autoreconnect = true & allowmultiqueries = trueJdbc.master.username = rootjdbc.master.password = 123456jd bc.slave01.driver = com.mysql.jdbc.driverjdbc.slave01.url = jdbc: mysql: //127.0.0.1: 3307/mybatis_1128? useunicode = true & Ch aracterencoding = utf8 & autoreconnect = true & allowmultiqueries = truejdbc.slave01.username = rootjdbc.slave01.password = 123456
3.5.2. Определите пул соединений
<!-Настройка пула соединений-> <bean id = "MasterDatasource" Dresser-method = "close"> <!-Драйвер базы данных-> <name = "DriverClass" value = "$ {jdbc.master.driver}" /> <!-соответствующий водитель jdbcurl-> <property name = "jdbcur". <!-Имя пользователя базы данных-> <name = "username" value = "$ {jdbc.master.username}" /> <!-Пароль базы данных-> <name = "password" value = "$ {jdbc.master.password}" /> <!-Проверьте время интервалов в пуле подключения DataBase. Устройство - часть. Значение по умолчанию составляет 240. Если вы хотите отменить, установите на 0.-> <name = "idleconnectionTestperiod" value = "60" /> <!-Максимальное количество соединений, которые не используются в пуле соединений. Устройство - часть. Значение по умолчанию составляет 60. Если вы хотите выжить навсегда, установите на 0.-> <name = "idlemaxage" value = "30" /> <!-Максимальное количество соединений на раздел-> <Название свойства = "maxConnectionspartpartition" value = "150" /> <!-Минимальное количество соединений на разделение-> <свойство name = "maxconnectionspartectionspartectorspartspartectorspartspartectorspartspartectorspartspartectorspartspartector Минимальное количество подключений на раздела-> <name = name = "maxconnectionsperpartition" value = "150" /> <!-Минимальное количество соединений на раздел-> <name = "minconnectionspartition" value = "5" /> < /bean> <!-Configure Connection Pool-> <Bean ID = "Slave01dAsource" Dissome-moth-methd = "rotectabase? Driver-> <name = name = "DriverClass" value = "$ {jdbc.slave01.driver}" /> <!-jdbcurl для соответствующего драйвера-> <name = "jdbcurl" value = "$ {jdbc.slave01.url}" /> <!-database username-> <pertive name = "usersame}" /> <!-database username-> <свойство. value = "$ {jdbc.slave01.username}" /> <!-пароль базы данных-> <name = "password" value = "$ {jdbc.slave01.password}" /> <!-Проверьте время интервала простальных соединений в базе базы данных. Устройство - часть. Значение по умолчанию составляет 240. Если вы хотите отменить, установите на 0.-> <name = "idleconnectionTestperiod" value = "60" /> <!-Максимальное время выживания неиспользованных ссылок в пуле соединений. Устройство - часть. Значение по умолчанию составляет 60. Если вы хотите выжить навсегда, установите на 0.-> <name = "idlemaxage" value = "30" /> <!-Максимальное количество соединений на раздел-> <name = "maxConnectionspartpartition" value = "150" /> <!-Минимальное количество соединений на разделение-> <propartice negnive "=" minconnectionspraterpartpartpartpartpartpartpartpartpartpartpartpartpartpartpartpartparte 3.5.3. Определить DataSource
<!-Определите источник данных и используйте источник данных, который вы реализуете-> <bean id = "dataSource"> <!-Установите несколько источников данных-> <name = "targetDataSources"> <Map Key-Type = "java.lang.string"> <!-Этот ключ должен соответствовать ключе в программе-> <intride. value-gaster-ref = "masterda-ref = key = "slave" value-ref = "slave01datasource"/> </map> </property> <!-Установите источник данных по умолчанию, здесь библиотека записи по умолчанию-> <name = "defaultTargetDataSource" ref = "MasterDataSource"/> </bean>
3.6 Настройка управления транзакциями и динамически переключить поверхности источника данных
3.6.1. Определение менеджера транзакций
<!-Определение транзакций менеджер-> <bean id = "transactionManager"> <name = "dataSource" ref = "dataSource" /> < /bean>
3.6.2. Определить политики транзакций
<!-Определить политику транзакции-> <TX: Advion ID = "txAdvice" Transaction-Manager = "TransactionManager"> <TX: атрибуты> <!-Определить методы запросов-только чтение-> <TX: name = "Query*" readmonly = "true" /> <tx: method name = "find*" read-only = "true" /> <tx " /> name" name = "name =" name "name =" name "name" name "name" = "name" name "nate" /"name?" /> <!-Основная библиотека выполняет операции, а поведение распространения транзакции определяется как поведение по умолчанию-> <TX: имя метода = "Сохранить*" Propagation = "Требуется" /> <TX: имя метода = "Обновление*" Propagation = "Требуется" /> <TX: имя метода = "DETETE*" ProPagation = " /> <!-другие методы. </tx: атрибуты> </tx: консультант>
3.6.3. Определите аспект
<!-Определите процессор раздела AOP-> <Bean Id = "DataSourCeasepect" /> <AOP: config> <!-определить разделы, все методы всех служб-> <AOP: pointcut id = "txpointcut" Expression = "exepress (*xx.xxx.xxxxx.service.*. <aop: Advisor ref-ref = "txadvice" pointcut-ref = "txpointcut"/> <!-Применить раздел к процессору пользовательского раздела, -9999, гарантирует, что раздел имеет высшее приоритетное выполнение-> <aop: Aspect ref = "dataSourcespect" order = "-9999"> <aop: до метода. pointcut-ref = "txpointcut"/> </aop: аспект> </aop: config>
4. Улучшение реализации раздела и используйте сопоставление правил политики транзакций
В предыдущей реализации мы сопоставляем имя метода вместо использования определения в политике транзакций, и мы будем использовать соответствие правила в политике управления транзакциями.
4.1. Улучшенная конфигурация
<!-Определите процессор раздела AOP-> <Bean id = "dataSourCeasepect"> <!-Укажите политику транзакции-> <name = "txadvice" ref = "txadvice"/> <!-Укажите префикс метода рабов (не требуется)-> <property name = "slavemethodstart" value = "Query, найдите, Get"/> </Bean> </Bean> </Bean> </bean> </bean> </bean> </bean> </bean> </bean> </bean> </bean>
4.2. Улучшенная реализация
Импорт java.lang.reflect.field; import java.util.arraylist; import java.util.list; импорт java.util.map; import org.apache.commons.lang3.stringutils; импорт org.aspectj.lang.joinpoint; импорт org.spramework.trancecter.nameChitestranceTranceTranceTranceTransCtorStranceTranceTranceTranceTranceTranceTranceTRAint org.springframework.transaction.interceptor.transactionattribute; import org.springframework.transaction.interceptor.transactionattributesource; импорт org.springframework.transaction.Interceptor.transactionInterceptor; импорт org.spramework.util.UTLIL. org.springframework.util.reflectionutils;/*** Определяет раздел AOP источника данных, который управляет тем, использовать ли мастер или раб. * * Если политика транзакций настроена в управлении транзакциями, метод маркировки Readonly в настроенной политике транзакций заключается в использовании рабов, а другой использует Master. * * Если нет политики для настройки управления транзакциями, принят принцип сопоставления имени метода, и подчинен используется в качестве начала с запроса, поиска и получения, а другие методы используются в качестве мастера. * * @author zhijun * */public class dataSourceSpect {private list <string> slavemethodpattern = new ArrayList <string> (); Частная статическая конечная строка [] defaultslavemethodstart = new String [] {"Query", "find", "get"}; частная строка [] SlaveMethodStart; / ** * Читать политики в управлении транзакциями * * @param txadvice * @throhs Exception */ @suppresswarnings ("uncecked") public void settxadvice (transactionInterceptor txAdvice) Обращает исключение {if (txadvice == null) {// Политика управления транзакциями не конфигурирует return; } // Получить информацию о конфигурации политики от txadvice Transactionattributesource Transactionattributesource = txadvice.getTransactionattributesource (); if (! (TransactionAttributesource exanceforf namematchtransactionattributesource)) {return; } // Использование технологии отражения для получения значения атрибута NAMEMAP в namematchtransactionattributesource объект namematchtransactionattributesource matchtransactionattributesource = (namematchtransactionattributesource) transactionattributesource; Field namemapfield = ReflectionUtils.findfield (namematchtransactionattributesource.class, "namemap"); namemapfield.setAccessible (true); // Установить это поле для доступа // Получить значение nameMap <string, transactionAttribute> map = (map <string, transactionattribute>) namemapfield.get (matchtransactionattributesource); // TransactionAttribute> intry: map.EntrySet ()) {if (! Entry.getValue (). IsReadOnly ()) {// После суждения политика Readonly определяется перед добавлением его в SlaveMethodPattern продолжить; } slavemethodpattern.add (entry.getKey ()); }} / *** Выполнить перед входом в метод службы* @param point face объект* / public void до (joinpoint point) {// Получить в данный момент имени метода выполненного метода methodname = point.getSignature (). GetName (); логический Isslave = false; if (slavemethodpattern.isempty ()) {// В текущем контейнере пружины не настроена политика транзакций, а метод сопоставления имени метода Isslave = Isslave (Methodname); } else {// Использовать правила политики, чтобы соответствовать (string mapedname: slavemethodpattern) {if (ismatch (methodname, mapedname)) {isslave = true; перерыв; }}}} if (isslave) {// mark как чтение библиотеки DynamicDataSourcelder.marksLave (); } else {// отмечать как библиотека записи DynamicDatasourceholder.markmaster (); }} / ** * Определите, является ли это библиотекой чтения * * @param methodname * @return * / private boolean isslave (string methodname) {// имя метода начинается с запроса, найти, получить return stringutils.startswithany (methodname, getslavemethodstart ()); } /** * Сопоставление с подстановочными знаками * * Вернуть, если данный имя метода соответствует отображенному имени. *<p>*Реализация по умолчанию проверяет для «XXX*», «*XXX» и «*XXX*», а также прямое*равенство. Может быть переопределен на подклассах. * * @param Методнамен Имя метода класса * @param mappenname Имя в дескрипторе * @return, если имена соответствуют * @see org.springframework.util.patternmatchutils#sommerematch (string, string) */ protected boolean ismatch (string methodname, string mappendame). } / *** Префикс имени метода указанного пользователя Slave* @param slavemethodstart* / public void setSlavemethodStart (String [] slavemethodStart) {this.SlavemethodStart = slaveMethodStart; } public String [] getSlaveMethodStart () {if (this.SlavemethodStart == null) {// не указан, используйте возврат по умолчанию defaultslavemethodstart; } return slavemethodstart; }}5. Реализация одного мастера и нескольких рабов
Во многих практических сценариях использования мы используем архитектуру «один мастер, многочисленные рабов», поэтому теперь мы поддерживаем эту архитектуру, и в настоящее время необходимо лишь модифицировать DynamicDataSource.
5.1. Выполнение
Импорт java.lang.reflect.field; import java.util.arraylist; import java.util.list; import java.util.map; import java.util.concurrent.atomic.atomicinteger; import javax.sql.datasource; import org.logger; org.springframework.jdbc.datasource.lookup.abstractroutingDatasource; import org.springframework.util.reflectionUtils;/** * Определить динамические источники данных и реализовать метод Abstractrout и невыполнение потока, Threadlocal используется для обеспечения безопасности потока, которая завершена DynamicDataSourceholder. * * @author zhijun * */public class dynamicdatasource extractroutingdatasource {private static final logger logger = loggerfactory.getlogger (dynamicdatasource.class); частное целочисленное славея; // Количество опросов, первоначально -1, Atomicinteger-это защитный частный счетчик Atomicinteger = новый Atomicinteger (-1); // Записать ключевой отдельный список <object> slavedatasources = new ArrayList <object> (0); @Override защищенного объекта DexteCurrentLookupkey () {// Использовать DynamicDatasourceholder для обеспечения безопасности потока и получить ключ источника данных в текущем потоке if (dynamicDatasourceholder.ismaster ()) {Object Key = DynamicDatasourceholder.getDataSourcekey (); if (logger.isdebugenabled ()) {logger.debug («Ключ текущего набора данных -:" + key); } return Key; } Объект Key = getSlaveKey (); if (logger.isdebugenabled ()) {logger.debug («Ключ текущего набора данных -:" + key); } return Key; } @Suppresswarnings ("unchecked") @override public void efpropertiesset () {super.afterpropertiesset (); // Поскольку свойство ResolvedDataSources родительского класса является частным подклассом, который не может быть получен, вам необходимо использовать отражение для получения Field Field = ReflectionUtils.findfield (AbstractroutingDatasource.class, "ResolvedDataSources"); Field.SetAccessible (true); // Установить доступность try {map <object, dataSource> resolvedDatasources = (map <object, dataSource>) field.get (this); // Размер данных в библиотеке чтения равен общему количеству источников данных за вычетом количества библиотек записи This.SlaveCount = ResolvedDataSources.Size () - 1; for (map.Entry <Object, dataSource> intry: resolvedDatasources.EntrySet ()) {if (dynamicDataSourceholder.master.equals (entry.getKey ())) {продолжение; } slavedatasources.add (entry.getKey ()); }} catch (Exception e) {logger.error ("effepropertiesset error!", e); }} / ** * Реализация алгоритма опроса * * @return * / public object getSlaveKey () {// Полученные подписки: 0, 1, 2, 3 ... целочисленный индекс = counter.incrementAndget () % slaveCount; if (counter.get ()> 9999) {// Чтобы избежать превышения целочисленного диапазона counter.set (-1); // восстановить} return slavedatasources.get (index); }}6. Репликация MySQL Master-Slave
6.1. Принцип
Принцип копирования MySQL Master (называемый мастером) раба (называемый раб):
1. Мастер записывает изменения данных в бинарном журнале, то есть файл, указанный в журнале файла конфигурации (эти записи называются событиями двоичного журнала, события двоичного журнала)
2. Рабовные копии бинарных бинарных веществ в его журнале реле (журнал ретрансляции)
3.
6.2. На что следует обратить внимание на конфигурацию Маха
1. Версии первичного сервера DB и базы данных Slave DB Server - это то же самое
2. Данные базы данных Master DB -сервера и подчиненного DB -сервера одинаковы [Здесь вы можете восстановить резервную копию мастера в подчинении, или вы можете напрямую скопировать каталог данных мастера в соответствующий каталог данных подчиненного]]
3. Основной сервер DB включает двоичные журналы, а основной сервер DB и сервер Slave DB должен быть уникальным.
6.3. Конфигурация основной библиотеки (аналогично Windows, Linux)
Некоторые друзья могут не иметь очень четкого IP -адреса, имени пользователя и конфигурации учетной записи базы данных Master и Slave. Ниже приводится конфигурация мастера и подчиненной, которую я протестировал. IPS все 127.0.0.1. После того, как я закончил свой пример, я напишу.
Мас-валн IP является примером различных конфигураций. Вы можете использовать этот пример, чтобы более интуитивно понять метод конфигурации.
Измените под my.ini [mysqld] (а также из библиотеки):
#Enable Master-Slave Replication, конфигурация основной библиотеки log-bin = mysql3306-bin#Укажите основную библиотеку Serveridserver-ID = 101#Укажите синхронизированную базу данных. Если не указано, все базы данных синхронизированы binlog-do-db = mybatis_1128
(Команды, введенные в my.ini, должны иметь место внизу места, в противном случае MySQL не узнает ее)
Выполнить Статус запроса оператора SQL: Показать статус Master
Значение позиции должна быть записана, а в библиотеке необходимо установить значение начала синхронизации.
Позвольте мне сказать еще одну вещь. Если вы выполняете статус Master Show в MySQL и обнаружите, что контент, настроенный в my.ini, не сработал. Может случиться так, что вы не выбрали файл my.ini, или, возможно, вы не перезагружали службу. Весьма вероятно, что это вызвано последним.
Чтобы конфигурация вступила в силу, вы должны отключить службу MySQL и перезапустить ее.
Как закрыть услугу:
Откройте ключ WIN, введите services.msc, чтобы позвонить в службу:
Начните SQLYOG снова и обнаружите, что конфигурация вступила в силу.
6.4. Создать синхронного пользователя в основной библиотеке
#Authorized Пользовательский Slav01 использует пароль 123456 для входа в систему MySQlgrant Replication Slave на *. * To 'slave01'@'127.0.0.1', идентифицированный '123456'; привилегии промывки;
6.5. Конфигурация из библиотеки
Изменить в my.ini:
#Specify ServerId, до тех пор, пока он не повторяется, из библиотеки существует только одна конфигурация, а другие работают в SQL Server-ID = 102
Следующее выполняет SQL (выполняет с помощью корневой учетной записи подчинения):
Changematertomater_hot = '127.0.0.1', // IP -адрес хоста Material_uer = 'lave01', // пользователь хоста (учетная запись, только что созданная на хосте через ql) mater_paword = '123456', mater_port = 3306, mater_log_file = 'myql3306-bin.000006', // filemate_log_po = 1120; // poition
#Start Slave Synchronization Start Slave; #View Статус синхронизации показать статус подчиненного;
Вот методы мастер -и подчиненной конфигурации для двух разных компьютеров IP:
Операционная система, в которой находится основная база данных: win7
Версия основной базы данных: 5.0
IP -адрес основной базы данных: 192.168.1.111
Из операционной системы, где находится база данных: Linux
Из версии данных: 5.0
IP -адрес из базы данных: 192.168.1.112
После введения среды давайте поговорим о шагах конфигурации:
1. Убедитесь, что основная база данных точно такая же, как и база данных рабов.
Например: база данных A в основной базе данных имеет таблицы B, C и D, поэтому база данных A и таблиц B, C и D должна быть выгравирована с помощью формы.
2. Создайте синхронную учетную запись в основной базе данных.
Кода -копия выглядит следующим образом:
Grant Replication Slave, File on *. * To 'mStest'@'192.168.1.112' Идентифицирован '123456';
192.168.1.112: это IP -адрес, который работает с использованием пользователя
MSTest: это недавно созданное имя пользователя
123456: это пароль недавно созданного имени пользователя
Подробное объяснение вышеупомянутой команды лучше всего выполнено на Baidu. Если вы напишите слишком много, это сделает это более неясным.
3. Настройте my.ini основной базы данных (потому что она находится под окном, это my.ini, а не my.cnf).
[mysqld] server-id = 1log-bin = logbinlog-do-db = mStest // Для синхронизации базы данных MSTEST, если вы хотите синхронизировать несколько баз данных, добавьте еще несколько Binlog-Do-DB = Имя базы данных Binlog-Vignore-db = MySQL //.
4. Настройте my.cnf из базы данных.
[mysqld]server-id=2master-host=192.168.1.111master-user=mstest //Step 1. Create the username of the account master-password=123456 //Step 1. Create the password of the account master-port=3306master-connect-retry=60replicate-do-db=mstest //To synchronize the mstest database, to synchronize multiple Базы данных, добавьте еще несколько Replicate-do-db = имя базы данных Replicate-ignore-db = mysql // база данных, которую следует игнорировать
5. Убедитесь, что это успешно
Введите MySQL и введите команду: показать статус рабов/g. Следующее изображение будет отображаться. Если Slave_io_running и Slave_sql_running - это да, это означает, что синхронизация может быть успешно
6. Проверьте синхронные данные.
Введите основную базу данных и введите команду: вставьте в одно значения (name) ('beijing');
Затем введите команду ввода из базы данных: выберите * из одного;
Если в настоящее время данные получены из базы данных, это означает, что синхронизация была успешной, а Master и Slave будут реализованы.
Выше всего содержание этой статьи. Я надеюсь, что это будет полезно для каждого обучения, и я надеюсь, что все будут поддерживать Wulin.com больше.