предисловие
Прошел месяц с тех пор, как я присоединился к новой компании, и я завершил начатую работу.Несколько дней назад у меня наконец-то появилось время изучить код старого проекта компании. В процессе исследования кода я обнаружил, что Spring Aop использовался в проекте для реализации разделения чтения и записи базы данных.Основываясь на моей любви к обучению (сам не верю в это...), я решил написать пример проекта, чтобы реализовать чтение и запись эффекта разделения spring aop.
Развертывание среды
База данных: MySql
Количество библиотек: 2, одна главная и одна подчиненная
Развертывание mysql в среде master-slave было описано ранее, поэтому я не буду здесь вдаваться в подробности.«Научите вас, как создать среду репликации master-slave mysql в системе Windows»
начать проект
В первую очередь, без сомнений, начните собирать проект SpringBoot, а затем внесите в pom-файл следующие зависимости:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- 动态数据源 所需依赖 ### start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>provided</scope>
</dependency>
<!-- 动态数据源 所需依赖 ### end-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
Структура каталогов
После введения основных зависимостей организуйте структуру каталогов.Скелет завершенного проекта примерно выглядит следующим образом:
построить таблицу
Создайте пользователя таблицы, выполните оператор sql в основной библиотеке и сгенерируйте соответствующие данные таблицы в подчиненной библиотеке.
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`user_name` varchar(255) DEFAULT '' COMMENT '用户名称',
`user_phone` varchar(50) DEFAULT '' COMMENT '用户手机',
`address` varchar(255) DEFAULT '' COMMENT '住址',
`weight` int(3) NOT NULL DEFAULT '1' COMMENT '权重,大者优先',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES ('1196978513958141952', '测试1', '18826334748', '广州市海珠区', '1', '2019-11-20 10:28:51', '2019-11-22 14:28:26');
INSERT INTO `user` VALUES ('1196978513958141953', '测试2', '18826274230', '广州市天河区', '2', '2019-11-20 10:29:37', '2019-11-22 14:28:14');
INSERT INTO `user` VALUES ('1196978513958141954', '测试3', '18826273900', '广州市天河区', '1', '2019-11-20 10:30:19', '2019-11-22 14:28:30');
Конфигурация источника данных ведущий-ведомый
application.yml, основная информация — конфигурация источника данных библиотеки master-slave
server:
port: 8001
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
master:
url: jdbc:mysql://127.0.0.1:3307/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password:
slave:
url: jdbc:mysql://127.0.0.1:3308/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password:
Поскольку имеется два источника данных, один главный и один подчиненный, вместо этого мы используем классы перечисления, чтобы мы могли соответствовать им, когда мы их используем.
@Getter
public enum DynamicDataSourceEnum {
MASTER("master"),
SLAVE("slave");
private String dataSourceName;
DynamicDataSourceEnum(String dataSourceName) {
this.dataSourceName = dataSourceName;
}
}
Класс информации о конфигурации источника данныхDataSourceConfig, тут настраиваются два источника данных, masterDb и slaveDb
@Configuration
@MapperScan(basePackages = "com.xjt.proxy.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {
// 主库
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDb() {
return DruidDataSourceBuilder.create().build();
}
/**
* 从库
*/
@Bean
@ConditionalOnProperty(prefix = "spring.datasource", name = "slave", matchIfMissing = true)
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDb() {
return DruidDataSourceBuilder.create().build();
}
/**
* 主从动态配置
*/
@Bean
public DynamicDataSource dynamicDb(@Qualifier("masterDb") DataSource masterDataSource,
@Autowired(required = false) @Qualifier("slaveDb") DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
if (slaveDataSource != null) {
targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
}
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sessionFactory(@Qualifier("dynamicDb") DataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
bean.setDataSource(dynamicDataSource);
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "dataSourceTx")
public DataSourceTransactionManager dataSourceTx(@Qualifier("dynamicDb") DataSource dynamicDataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dynamicDataSource);
return dataSourceTransactionManager;
}
}
установить маршрут
Цель установки маршрута Чтобы облегчить поиск соответствующего источника данных, мы можем использовать ThreadLocal для сохранения информации источника данных в каждом потоке, чтобы мы могли получить ее, когда она нам понадобится.
public class DataSourceContextHolder {
private static final ThreadLocal<String> DYNAMIC_DATASOURCE_CONTEXT = new ThreadLocal<>();
public static void set(String datasourceType) {
DYNAMIC_DATASOURCE_CONTEXT.set(datasourceType);
}
public static String get() {
return DYNAMIC_DATASOURCE_CONTEXT.get();
}
public static void clear() {
DYNAMIC_DATASOURCE_CONTEXT.remove();
}
}
получить маршрут
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
Функция AbstractRoutingDataSource заключается в маршрутизации к соответствующему источнику данных на основе ключа поиска.Он поддерживает набор целевых источников данных внутри, выполняет сопоставление между ключом маршрутизации и целевым источником данных и предоставляет метод для поиска источников данных. на основе ключа.
Аннотация к источнику данных
Чтобы облегчить переключение источников данных, мы можем написать аннотацию, которая содержит значение перечисления, соответствующее источнику данных, по умолчанию это основная библиотека,
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSelector {
DynamicDataSourceEnum value() default DynamicDataSourceEnum.MASTER;
boolean clear() default true;
}
источник данных переключения aop
В этот момент может наконец появиться aop. Здесь мы определяем класс aop для переключения источника данных для аннотированного метода. Конкретный код выглядит следующим образом:
@Slf4j
@Aspect
@Order(value = 1)
@Component
public class DataSourceContextAop {
@Around("@annotation(com.xjt.proxy.dynamicdatasource.DataSourceSelector)")
public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
boolean clear = true;
try {
Method method = this.getMethod(pjp);
DataSourceSelector dataSourceImport = method.getAnnotation(DataSourceSelector.class);
clear = dataSourceImport.clear();
DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());
log.info("========数据源切换至:{}", dataSourceImport.value().getDataSourceName());
return pjp.proceed();
} finally {
if (clear) {
DataSourceContextHolder.clear();
}
}
}
private Method getMethod(JoinPoint pjp) {
MethodSignature signature = (MethodSignature)pjp.getSignature();
return signature.getMethod();
}
}
На этом наша работа по подготовке и настройке завершена, и ниже начинается тестовый эффект.
Сначала напишите служебный файл, включая два метода чтения и обновления,
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
public List<User> listUser() {
List<User> users = userMapper.selectAll();
return users;
}
@DataSourceSelector(value = DynamicDataSourceEnum.MASTER)
public int update() {
User user = new User();
user.setUserId(Long.parseLong("1196978513958141952"));
user.setUserName("修改后的名字2");
return userMapper.updateByPrimaryKeySelective(user);
}
@DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
public User find() {
User user = new User();
user.setUserId(Long.parseLong("1196978513958141952"));
return userMapper.selectByPrimaryKey(user);
}
}
По аннотации к методу видно, что метод чтения идет из ведомой библиотеки, метод обновления идет в основную библиотеку, а обновляемый объект - это userId как1196978513958141953
Данные,
Затем мы пишем тестовый класс, чтобы проверить, можно ли добиться эффекта,
@RunWith(SpringRunner.class)
@SpringBootTest
class UserServiceTest {
@Autowired
UserService userService;
@Test
void listUser() {
List<User> users = userService.listUser();
for (User user : users) {
System.out.println(user.getUserId());
System.out.println(user.getUserName());
System.out.println(user.getUserPhone());
}
}
@Test
void update() {
userService.update();
User user = userService.find();
System.out.println(user.getUserName());
}
}
Результаты теста:
1. Метод чтения
2. Метод обновления
После выполнения можно обнаружить, сравнив базу данных, что главная и подчиненная библиотеки изменили данные, указывая на то, что наше разделение чтения-записи выполнено успешно. Конечно, метод обновления может указывать на ведомую библиотеку, так что будут изменены только данные ведомой библиотеки, а основная библиотека не будет задействована.
Уведомление
Хотя протестированный выше пример относительно прост, он также соответствует традиционной конфигурации разделения чтения-записи. Стоит отметить, что роль разделения чтения-записи заключается в том, чтобы снять нагрузку с написания библиотеки, то есть основной библиотеки, но оно должно быть основано на принципе согласованности данных, то есть гарантировать, что данные между главные и подчиненные библиотеки должны быть согласованы.Если метод включает в себя написание логики, все операции с базой данных в методе должны выполняться в основной библиотеке..
Предполагая, что данные могут не синхронизироваться в подчиненной библиотеке после выполнения операции записи, а затем начинает выполняться операция чтения.Если программа чтения все еще из подчиненной библиотеки, будет несогласованность данных.Это не допускается нами.
Наконец, отправьте адрес проекта на github. Заинтересованные студенты могут его прочитать. Не забудьте поставить звезду.
адрес:GitHub.com/тао XJ/MySQL…