Os dados do banco de dados no projeto lançado recentemente estão se aproximando da saturação. Os maiores dados da tabela são próximos de 3000W e existem várias tabelas com milhões de dados. O projeto exige que o tempo de leitura de dados não possa exceder 0,05 segundos, mas a situação real não atende aos requisitos. Explique a indexação e o uso da tecnologia Redis e Ehcache Cache não podem mais atender aos requisitos. Portanto, começamos a usar a tecnologia de separação de leitura e gravação. Talvez quando o volume de dados exceder 100 milhões ou mais no futuro, precisamos considerar a implantação de bancos de dados distribuídos. No entanto, atualmente, leia e grava separação + cache + índice + partição da tabela + otimização SQL + balanceamento de carga pode atender ao trabalho de consulta de 100 milhões de volumes de dados. Vamos dar uma olhada nas etapas para usar a primavera para alcançar a separação de leitura e gravação:
1. Antecedentes
Nosso aplicativo geral é "ler mais e escrever menos" para bancos de dados, o que significa que a pressão no banco de dados para ler dados é relativamente alta. Uma idéia é usar uma solução de cluster de banco de dados.
Um deles é a biblioteca principal, responsável por escrever dados, que chamamos: Writing Library;
Os outros são todos da biblioteca, responsável pela leitura de dados, que chamamos: Reading Library;
Portanto, os requisitos para nós são:
1. Os dados da biblioteca de leitura e da biblioteca de gravação são consistentes; (Esta é uma questão muito importante. O processamento da lógica de negócios deve ser processado na camada de serviço, e não no nível DAO ou Mapper)
2. Ao escrever dados, você deve escrevê -los na biblioteca de redação;
3. Você deve ir à biblioteca de leitura para ler os dados;
2. Plano
Existem duas soluções para resolver a separação de leitura e gravação: solução da camada de aplicativo e solução de middleware.
2.1. Solução da camada de aplicativo:
vantagem:
1. Várias fontes de dados são fáceis de alternar e são concluídas automaticamente pelo programa;
2. Nenhum middleware é necessário;
3. Em teoria, apoie qualquer banco de dados;
deficiência:
1. Concluído por programadores, e operação e manutenção não estão envolvidas;
2. Aumentar dinamicamente as fontes de dados não podem ser alcançadas;
2.2. Middleware Solution
Prós e contras:
vantagem:
1. O programa de origem pode alcançar a separação de leitura e gravação sem nenhuma alteração;
2. Adicionar fontes de dados dinamicamente não requer reiniciar o programa;
deficiência:
1. Os programas dependem do middleware, o que dificulta a troca de bancos de dados;
2. O middleware é usado como agente de trânsito e o desempenho diminuiu;
3. Use a primavera para implementar com base na camada de aplicativo
3.1. Princípio
Antes de inserir o serviço, use a AOP para fazer um julgamento, se deve usar uma biblioteca de gravação ou uma biblioteca de leitura, a base do julgamento pode ser julgada com base no nome do método, como o que começa com a consulta, encontrar, obter etc. e outras bibliotecas de gravação.
3.2. DynamicDataSource
importar org.springframework.jdbc.dataSource.lookup.abstractoutingDataSource;/*** Definir fontes de dados dinâmicos e implementar abstractroutingDataSource fornecido pela Integration Spring. Você só precisa implementar o método DetermineCurrentLookupKey * * Como o DynamicDataSource é um singleton e inseguro de threads, o ThreadLocal é usado para garantir a segurança do fio, que é concluído pelo DynamicDataSourceholder. * * @author zhijun * */public class DynamicDataSource estende abstractroutingDataSource {@Override Protected Object DetermineCurrentLookKey () {// use dinâmicoDataSourceHolder para garantir a segurança do encadeamento e obter a chave da fonte de dados na corrente de retorno dinâmicoDataData. }} 3.3. DynamicDataSourceholder
/** * * Use a tecnologia ThreadLocal para gravar a chave da fonte de dados no thread atual * * @author zhijun * */public class DynamicDataSourceholder {// Escreva a fonte de dados correspondente à biblioteca private estática final Static Final String Master = "Master"; // Leia a fonte de dados correspondente à biblioteca String estática privada escravo = "escravo"; // Use Threadlocal para gravar a fonte de dados do thread atual estático privado Final ThreadLocal <String> holder = new ThreadLocal <String> (); / ** * Defina a chave da fonte de dados * @param key */ public static void putDataSourceKey (chave de string) {holder.set (key); } / ** * Obtenha a chave da fonte de dados * @return * / public static string getDataSourceKey () {return holder.get (); } / *** Biblioteca de gravação de marcação* / public static void Markmaster () {putdataSourceKey (mestre); } / *** Markup Read Library* / public static void Markslave () {putdataSourceKey (escravo); }} 3.4. DataSourCeaspect
importar org.apache.commons.lang3.stringUtils; importar org.aspectj.lang.joinpoint;/** * Definir a seção AOP da fonte de dados e julgar se é hora de ler a biblioteca ou escrever a biblioteca através do nome do método do serviço * @Author Zhijun * */public class DataSourCeas */ public void antes (ponto de junta) {// Obtenha o nome do método atualmente executado String MethodName = Point.getSignature (). getName (); if (iSSLave (MethodName)) {// Mark como LEAD Library DynamicDataSourceholder.MarksLave (); } else {// Mark como Write Library DynamicDataSourceholder.markmaster (); }} / ** * Determine se é uma biblioteca de leitura * * @param MethodName * @return * / private boolean iSSLave (string métodname) {// o nome do método começa com consulta, encontre, obtenha, retorna stringils.startswithany (métodname, "Query", "Find", "Get"); }}3.5. Configurar 2 fontes de dados
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. Defina o pool de conexão
<!-Configurar pool de conexão-> <bean id = "masterDataSource" de destruir-method = "close"> <!-driver de banco de dados-> <propriedade name = "driver" value = "$ {jdbc.master.driver}" /> <!-o driver correspondente "jdbcurl-> name =" /> <!-O nome de usuário do banco de dados-> <propriedade name = "userName" value = "$ {jdbc.master.username}" /> <!-a senha do banco de dados-> <nome da propriedade "senha" Value = "$ {JDBC.Master.Password}" /> <! A unidade é uma fração. O valor padrão é 240. Se você deseja cancelar, defina como 0.-> <propriedade name = "IdleConnectionTestPeriod" value = "60" /> <!-O número máximo de conexões que não são usadas no pool de conexões. A unidade é uma fração. O valor padrão é 60. Se você deseja sobreviver para sempre, definido como 0.-> <Propriedade name = "idlemaxage" value = "30" /> <!-o número máximo de conexões por partição-> <names name = "maxConnectionSperPartition" Value = "150" /> <!-O número mínimo de conexões) <! O número mínimo de conexões por partição-> <Nome da propriedade = "maxConnectionsPerPartition" value = "150" /> <!-O número mínimo de conexões por partição-> <Nome da propriedade "MINCONNCIONCSIONCIONSPERTITITION" Value = "5" /> < /Bean> <!-Configurar Pool de conexão-> <Bean "" Slaves = "5" /> < /Bean> <!-Configurar pool de conexão-> <Bean = " driver-> <propriedade name = "driverclass" value = "$ {jdbc.slave01.driver}" /> <!-jdbcurl para o driver correspondente-> <names name = "jdbcurl" value = "$ {jdbc.lave01.url}" /> <! value = "$ {jdbc.slave01.username}" /> <!-a senha do banco de dados-> <propriedade name = "senha" value = "$ {jdbc.slave01.password}" /> <!-verifique o tempo de intervalo das conexões de interface no pool de conexão com dados do DataBase. A unidade é uma fração. O valor padrão é 240. Se você deseja cancelar, defina como 0.-> <propriedade name = "IdleConnectionTestPeriod" value = "60" /> <!-O tempo máximo de sobrevivência dos links não utilizados no pool de conexões. A unidade é uma fração. O valor padrão é 60. Se você deseja sobreviver para sempre, definido como 0.-> <Propriedade name = "idlemaxage" value = "30" /> <!-o número máximo de conexões por partição-> <nome da propriedade "maxConnectionSperPartition" Value = "150" /> <!-Número mínimo de conexões Per partindo-> <! 3.5.3. Defina o DataSource
<!-Defina a fonte de dados e use a fonte de dados que você implementa-> <bean id = "DataSource"> <!-Defina várias fontes de dados-> <propriedades name = "TargetDataSources"> <map key-type = "java.lang.string"> <!-essa chave precisa ser consistente com a chave no programa-> <Entrada) key = "escravo" value-ref = "slave01DataSource"/> </map> </propriedade> <!-Defina a fonte de dados padrão, aqui a biblioteca de gravação padrão-> <names name = "defaultTargetDataSource" ref = "masterDataSource"/> </siean>
3.6. Configurar gerenciamento de transações e alternar dinamicamente as superfícies da fonte de dados
3.6.1. Definindo o gerente de transação
<!-Definition Transaction Manager-> <bean id = "transactionManager"> <propriedade name = "DataSource" ref = "DataSource" /> </shean>
3.6.2. Defina políticas de transação
<!-Definir política de transação-> <tx: conselhos id = "txadvice" transaction-manager = "transactionManager"> <tx: atributes> <!-define métodos de consulta são leitura- leitura somente = "true" /> <!-A biblioteca principal executa operações, e o comportamento da propagação da transação é definido como o comportamento padrão-> <tx: método name = "save*" propagation = "requerir" /> <tx: método name = "update*" propagação = "requerir /> <tx: método name =" despete*"propagação" "" <tx: método name = "*"/> </tx: atributes> </tx: conselhos>
3.6.3. Defina a faceta
<!-Definir Processador da Seção AOP-> <Bean id = "DataSourCeaspect" /> <AOP: Config> <!-Definir seções, todos os métodos de todos os serviços-> <AOP: PointCUT ID = "TXPOINTCUT" Expression = "Execution (*XX.XXX.xxxxxx.Service". <AoP: Advisor Advice-ref = "Txadvice" Pointcut-ref = "TxPointCut"/> <!-Aplique a seção a um processador de seção personalizado, -9999 garante que a seção tenha a mais alta prioridade. /> </AOP: Aspect> </aOP: config>
4. Melhore a implementação da seção e use a correspondência de regras de política de transação
Na implementação anterior, corresponderemos ao nome do método em vez de usar a definição na política de transações e usaremos a correspondência de regras na política de gerenciamento de transações.
4.1. Configuração aprimorada
<!-Definir processador da seção AOP-> <bean id = "DataSourCeaspect"> <!-especificar política de transação-> <propriedade name = "txadvice" ref = "txadvice"/> <!-especificar o prefixo do método escravo (não é necessário)-> <Nome da propriedade "/" SLAVEMETHEMT "
4.2. Implementação aprimorada
importar java.lang.reflect.field; importar java.util.ArrayList; importar java.util.list; importar java.util.map; importar org.apache.commons.lang3.stringUtils; importação.aspectj.lang.Join; org.springframework.transaction.intercept.TransactionAttribute; importar org.springframework.transaction.intercept.TransactionAttributesource; importar org.springframework.transaction.intercept.transintercept; importar org.springframework.util.patter.patter.Transintercept; importar org.springframework.util. org.springframework.util.reflectionUtils;/*** Define a seção AOP da fonte de dados, que controla se deve usar mestre ou escravo. * * Se uma política de transação estiver configurada no gerenciamento de transações, o método de marcar readonly na política de transação configurada é usar escravo e o outro usa o mestre. * * Se não houver política para configurar o gerenciamento de transações, o princípio da correspondência de nomes do método é adotado e o escravo é usado como começando com consulta, localização e obtenção e outros métodos são usados como mestre. * * @author zhijun * */public class DataSourCeaspect {Private List <String> slaveMethodPattern = new ArrayList <String> (); String final estática privada [] defaultSlaveMethodStart = new String [] {"Query", "Find", "Get"}; String privada [] SlaveMethodStart; / ** * Leia as políticas no gerenciamento de transações * * @param txadvice * @THOWSOWS Exceção */ @suppresswarnings ("desmarcada") public void Settxadvice (transactionInterceptor txadvice) lança a exceção {if (txadvice == null) {// A política de gerenciamento de transação não é confundida; } // Obtenha informações de configuração da política da transação txadviceTtributesource transactionAttributesource = txadvice.gettransactionAttributesource (); if (! (transactionAttributesource Instância de nameMatchTransactionAttributesource)) {return; } // Use a tecnologia de reflexão para obter o valor do atributo NameMap no NameMatchTransactionAttributesource Object NameMatchTransactionAttributesOrce MatchTransactionAttributesource = (NameMatchTransactionAttribUtece) transactionAttributesource; Campo nameMapfield = refletionUtils.findfield (nameMatchTransactionAttributesource.class, "NameMap"); namemapfield.setAccessible (true); // Defina este campo para acessar // Obtenha o valor de nameMap <string, transactionAttribute> map = (map <string, transactionAttribute>) namemapfield.get (matchTransactionAttributesource); // transactionAttribute> Entrada: map.entrySet ()) {if (! Entry.getValue (). IsReadenly ()) {// Após o julgamento, a política de leitura é definida antes de adicioná -la ao SlaveMethodPattern, continue; } slaveMethodPattern.add (Entry.getKey ()); }} / *** Execute antes de inserir o método de serviço* @param Point Face Object* / public void Antes (ponto de junta) {// Obtenha o nome do método atualmente executado String MethodName = Point.getSignature (). GetName (); boolean Isslave = false; if (slaveMethodPattern.isEmpty ()) {// não existe uma política de transação configurada no contêiner atual da mola e o método de correspondência de nome do método ISSLAVE = ISSLAVE (MethodName); } else {// use regras de política a serem correspondentes (string mapedname: slaveMethodPattern) {if (ismatch (métodname, mapedname)) {iSsLave = true; quebrar; }}}} if (issLave) {// Mark como leitura da biblioteca dinâmicaDataSourceholder.markslave (); } else {// Mark como Write Library DynamicDataSourceholder.markmaster (); }} / ** * Determine se é uma biblioteca de leitura * * @param MethodName * @return * / private boolean IssLave (string métodname) {// nome do método começa com consulta, encontre, obtenha retorno stringils.startswithany (MethodName, getSLaVeThodStart ()); } /** * correspondência de curinga * * Retorne se o nome do método fornecido corresponder ao nome mapeado. *<p>*A implementação padrão verifica "xxx*", "*xxx" e "*xxx*" corresponde, além de direto*igualdade. Pode ser substituído em subclasses. * * @Param MethodName O nome do método da classe * @param mapedname o nome no descritor * @return Se os nomes correspondem * @see org.springframework.util.patternmatchutils#simplematch (string, string) */ protegido boolean ismatch (stringSes. string, mapedname) {strorn) {strorn). MethodName); } / *** O nome do método prefixo do usuário escravo especificado* @param slaveMethodStart* / public void setSlAveMethodStart (String [] slaveMethodStart) {this.laveMethodStart = slaveMethodStart; } public String [] getSLaveMethodStart () {if (this.slaveMethodStart == null) {// não especificado, use o retorno padrão defaultSlaveMethodStart; } retornar SlaveMethodStart; }}5. Implementação de um mestre e vários escravos
Em muitos cenários de uso prático, usamos a arquitetura "um mestre, múltiplo escravo", por isso agora apoiamos essa arquitetura e atualmente precisamos apenas modificar o DynamicDataSource.
5.1. Implementação
importar java.lang.reflect.field; importar java.util.ArrayList; importar java.util.list; importar java.util.map; importar java.util.concurrent.atomic.atomicinteger; import javax.sqlger.datasource; importação ousld.sld.slrgger; org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import org.springframework.util.ReflectionUtils;/** * Define dynamic data sources and implement AbstractRoutingDataSource provided by Spring through integration, you only need to implement the determineCurrentLookupKey method * * Since DynamicDataSource is a singleton e inseguro de threads, o Threadlocal é usado para garantir a segurança do fio, que é concluído pelo dinâmicoDataSourceholder. * * @author zhijun * */public class DynamicDataSource estende abstractroutingDataSource {private static final logger logger = LoggerFactory.getLogger (DynamicDatasource.class); private INTEGER SLAVECOUNT; // Contagem de pesquisas, inicialmente -1, o AtomicInteger é um contador atômico privado segura para roscas = novo atomicinteger (-1); // Registre a lista principal da lista privada <BECT> SlaveDataSources = new ArrayList <ject> (0); @Override Protected Object determineCurrentLeoveKey () {// Use DynamicDataSourceholder para garantir a segurança do thread e obter a chave da fonte de dados no thread atual se (dinâmicodataSourceholder.ismaster ()) {key key = dynamicdatasourceholder.getDataSourceKey ()); if (logger.isdebugenabled ()) {Logger.debug ("A chave do conjunto de dados atual é:" + key); } chave de retorno; } Chave do objeto = getsLaveKey (); if (logger.isdebugenabled ()) {Logger.debug ("A chave do conjunto de dados atual é:" + key); } chave de retorno; } @Suppresswarnings ("desmarcado") @Override public void depoisPROPERTIESTET () {super.afterPropertiesset (); // Como a propriedade ResolvedDataSources da classe pai é uma subclasse privada que não pode ser obtida, você precisa usar a reflexão para obter o campo de campo = reflexoTils.findfield (abstractoutingDataSource.class, "ResolvedDataSources"); field.setAccessible (true); // Definir acessibilidade, tente {map <object, DataSource> resolvedDataSources = (map <objeto, dataSource>) field.get (this); // A quantidade de dados na biblioteca de leitura é igual ao número total de fontes de dados menos o número de bibliotecas de gravação this.SlaveCount = ResolvedDataSources.size () - 1; para (map.entry <objeto, DataSource> Entrada: ResolvedDataSources.EntrySet ()) {if (DynamicDataSourceholder.master.equals (entradas.getKey ())) {Continue; } slaveDataSources.add (Entry.getKey ()); }} Catch (Exceção e) {Logger.error ("AfterPropertiesset Error!", E); }} / ** * Implementação do algoritmo de pesquisa * * @return * / public Object getSLaveKey () {// Os subscritos resultantes são: 0, 1, 2, 3 ... índice inteiro = contador.incremendget () % de escravo; if (count.get ()> 9999) {// para evitar exceder o contador de intervalo inteiro.set (-1); // restaurar} retornar slaveDatasources.get (index); }}6. Replicação do MySQL Master-Slave
6.1. Princípio
O princípio de copiar o MySQL Master (chamado mestre) escravo (chamado escravo):
1. O mestre registra as mudanças de dados no log binário, ou seja, o arquivo especificado pelo log-bin de arquivo de configuração (esses registros são chamados de eventos binários de log, eventos binários de log)
2. Logvents binários do escravo da cópia do mestre em seu log de relé (registro de relé)
3. Os eventos de retrabalho de escravos no registro de relé alterarão os dados que refletem -se (replays de dados)
6.2. O que deve receber atenção quando a configuração mach
1. As versões do servidor DB primário e o banco de dados de servidor de db de escravo são iguais
2. Os dados do banco de dados do servidor mestre do banco de dados e do servidor de DB de escravos são os mesmos [aqui você pode restaurar o backup do mestre no escravo, ou você pode copiar diretamente o diretório de dados do mestre para o diretório de dados do escravo correspondente]
3. O servidor DB principal permite logs binários e o servidor DB principal e o servidor de DB de escravo devem ser únicos.
6.3. Configuração principal da biblioteca (semelhante ao Windows, Linux)
Alguns amigos podem não ter endereço IP muito claro, nome de usuário e configuração de conta do banco de dados Master e Slave. A seguir, a configuração mestre e escravo que testei. Os IPs são todos 127.0.0.1. Depois que terminei meu exemplo, vou escrever.
Um IP mestre-escravo é um exemplo de configurações diferentes. Você pode usar este exemplo para entender o método de configuração mais intuitivamente.
Modifique em My.ini [mySqld] (também na biblioteca):
#Enable Replicação mestre-escravo, a configuração da biblioteca principal Log-bin = MySQL3306-Bin#Especifique o principal servidor da biblioteca serverIdServer-id = 101#Especifique o banco de dados sincronizado. Se não for especificado, todos os bancos de dados são sincronizados binlog-do-db = mybatis_1128
(Os comandos inseridos em My.ini devem ter uma linha de espaço abaixo, caso contrário, o MySQL não o reconhecerá)
Executar o status da consulta da instrução SQL: Mostrar status mestre
O valor da posição precisa ser registrado e o valor inicial de sincronização precisa ser definido na biblioteca.
Deixe -me dizer mais uma coisa. Se você executar o status mestre do Moster no MySQL e descobrir que o conteúdo configurado em my.ini não funcionou. Pode ser que você não tenha escolhido o arquivo my.ini, ou pode não reiniciar o serviço. É muito provável que seja causado pelo último.
Para fazer a configuração entrar em vigor, você deve desativar o serviço MySQL e reiniciá -lo.
Como fechar o serviço:
Abra a chave WIN, digite o Service.msc para chamar o serviço:
Inicie o Sqlyog novamente e descobri que a configuração entrou em vigor.
6.4. Crie um usuário síncrono na biblioteca principal
#Authorized User Slave01 usa a senha 123456 para fazer login no escravo de replicação MySQLGrant em *.
6.5. Configuração da biblioteca
Modificar em my.ini:
#Specify ServerID, desde que não seja repetido, existe apenas uma configuração da biblioteca e os outros são operados no SQL Declarect Server-ID = 102
O seguinte executa o SQL (executa usando a conta raiz do escravo):
Changematertomater_hot = '127.0.0.1', // O endereço IP do host material_uer = 'Lave01', // O usuário do host (a conta recém -criada no host através ql) Mater_paword = '123456', Mater_port = 3306, Mater_Log_file = 'MyQL3306-Bin.000006', // filemate_log_po = 1120; // poition
#Start Sincronização de escravos escravo;
Aqui estão os métodos de configuração mestre e escravo para dois computadores IP diferentes:
O sistema operacional onde reside o banco de dados principal: Win7
Versão do banco de dados primário: 5.0
O endereço IP do banco de dados principal: 192.168.1.11111
Do sistema operacional onde reside o banco de dados: Linux
Da versão dos dados: 5.0
Endereço IP do banco de dados: 192.168.1.112
Depois de apresentar o ambiente, vamos falar sobre as etapas de configuração:
1. Verifique se o banco de dados mestre é exatamente o mesmo que o banco de dados de escravos.
Por exemplo: o banco de dados de A no banco de dados principal possui tabelas B, C e D, portanto, o banco de dados de A e Tabelas B, C e D deve ser gravado com um molde.
2. Crie uma conta síncrona no banco de dados principal.
A cópia do código é a seguinte:
Conceder escravo de replicação, arquivo *. * Para 'mstest'@'192.168.1.112' identificado por '123456';
192.168.1.112: É o endereço IP que é executado usando o usuário
MSTEST: é o nome de usuário recém -criado
123456: é a senha do nome de usuário recém -criado
A explicação detalhada do comando acima é melhor feita no Baidu. Se você escrever muito, isso tornará mais claro.
3. Configure my.ini do banco de dados principal (porque está sob a janela, é meu.ini e não meu.cnf).
[mysqld] server-id = 1log-bin = logbinlog-dO-db = mstest // para sincronizar o banco de dados MSTEST, se você deseja sincronizar vários bancos de dados, adicione mais alguns binlog-db-db = database names binlog-db = mysql // ao soma do banco de dados
4. Configure my.cnf no banco de dados.
[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 Bancos de dados, adicione mais alguns replicados-db = nome do banco de dados replicar-se-db = mysql // o banco de dados a ser ignorado
5. Verifique se é bem -sucedido
Digite MySQL e digite o comando: mostre status de escravo/g. A imagem a seguir será exibida. Se tanto Slave_IO_Running quanto Slave_SQL_Running forem sim, isso significa que a sincronização pode ser com sucesso
6. Dados síncronos de teste.
Digite o banco de dados principal e digite o comando: insert em um (nome) valores ('beijing');
Em seguida, digite o comando de entrada no banco de dados: selecione * de um;
Se os dados forem recuperados do banco de dados no momento, significa que a sincronização foi bem -sucedida e o mestre e o escravo serão implementados.
O exposto acima é todo o conteúdo deste artigo. Espero que seja útil para o aprendizado de todos e espero que todos apoiem mais o wulin.com.