瀏覽代碼

增加了beanutils的报错笔记
增加了springboot多数据源配置的笔记
增加了事务导致多数据源切换的笔记

seamew 2 年之前
父節點
當前提交
e08365f0ac

+ 0 - 0
BUG/后端/.md


+ 91 - 0
BUG/后端/BeanUtils报错.md

@@ -0,0 +1,91 @@
+## 问题复现
+
+在使用第三方工具类BeanUtils的getProperty方法时候遇见的奇怪的BUG。
+
+* 实体类UserTakeActivity
+
+```java
+@Data
+public class UserTakeActivity {
+    /**
+     * 自增ID
+     */
+    private Long id;
+    /**
+     * 用户ID
+     */
+    private String uId;
+}
+```
+
+* 测试方法报错
+
+```java
+@Test
+public void test_insert() throws Exception {
+    UserTakeActivity userTakeActivity = new UserTakeActivity();
+    userTakeActivity.setUId("Uhdgkw766120d"); // 1库:Ukdli109op89oi 2库:Ukdli109op811d
+    System.out.println(BeanUtils.getProperty(userTakeActivity, "uId"));
+}
+```
+
+![image-20230223144613302](assets/image-20230223144613302.png)
+
+* 换成UId就不报错
+
+```java
+@Test
+public void test_insert() throws Exception {
+    UserTakeActivity userTakeActivity = new UserTakeActivity();
+    userTakeActivity.setUId("Uhdgkw766120d"); // 1库:Ukdli109op89oi 2库:Ukdli109op811d
+    System.out.println(BeanUtils.getProperty(userTakeActivity, "UId"));
+}
+```
+
+![image-20230223144726612](assets/image-20230223144726612.png)
+
+## 问题原因
+
+查看`BeanUtils`的源码
+
+```java
+private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) {
+    if (beanClass == null) {
+        throw new IllegalArgumentException("No bean class specified");
+    }
+
+    // Look up any cached information for this bean class
+    // 获取data元信息
+    BeanIntrospectionData data = descriptorsCache.get(beanClass);
+    if (data == null) {
+        data = fetchIntrospectionData(beanClass);
+        descriptorsCache.put(beanClass, data);
+    }
+
+    return data;
+}
+```
+
+该方法导致获取name为UId,与实际的不一致
+```java
+public static String decapitalize(String name) {
+    if (name == null || name.length() == 0) {
+        return name;
+    }
+    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
+        Character.isUpperCase(name.charAt(0))){
+        return name;
+    }
+    char chars[] = name.toCharArray();
+    chars[0] = Character.toLowerCase(chars[0]);
+    return new String(chars);
+}
+```
+
+BeanUtils.getProperty遵循Java Beans规范的java.beans.Introspectors行为。
+
+以后对变量进行命名不要第一个和第二个字母都是大写。会导致歧义
+
+## 参考链接
+
+[BEANUTILS-369](https://issues.apache.org/jira/browse/BEANUTILS-369)

二進制
BUG/后端/assets/image-20230223144613302.png


二進制
BUG/后端/assets/image-20230223144726612.png


+ 17 - 17
后端/Java/JAVA高阶/JUC编程/Threadlocal.md

@@ -29,7 +29,7 @@ B. Threadlocal在每个线程中都有自己独立的局部变量,空间换时
 1. 在每个线程中都有自己独立的 ThreadLocalMap 对象,中 Entry 对象。
 1. 在每个线程中都有自己独立的 ThreadLocalMap 对象,中 Entry 对象。
 2. 如果当前线程对应的的 ThreadLocalMap 对象为空的情况下,则创建该 ThreadLocalMap对象,并且赋值键值对。
 2. 如果当前线程对应的的 ThreadLocalMap 对象为空的情况下,则创建该 ThreadLocalMap对象,并且赋值键值对。
 Key 为 当前 new ThreadLocal 对象,value 就是为 object 变量值。
 Key 为 当前 new ThreadLocal 对象,value 就是为 object 变量值。
-![image-20211101152903627](../../../照片/image-20211101152903627.png)
+![image-20211101152903627](assets/image-20211101152903627.png)
 
 
 ```java
 ```java
 	// Threadlocal的set方法
 	// Threadlocal的set方法
@@ -86,27 +86,27 @@ threadLocalMap.get(ThreadLocal)-----缓存变量值
 1. 可以自己调用 remove 方法将不要的数据移除避免内存泄漏的问题;
 1. 可以自己调用 remove 方法将不要的数据移除避免内存泄漏的问题;
 2. 每次在做 set 方法的时候会清除之前 key 为 null;
 2. 每次在做 set 方法的时候会清除之前 key 为 null;
 ```java
 ```java
-            for (Entry e = tab[i];
-                 e != null;
-                 e = tab[i = nextIndex(i, len)]) {
-                ThreadLocal<?> k = e.get();
-
-                if (k == key) {
-                    e.value = value;
-                    return;
-                }
-
-                if (k == null) {
-                    replaceStaleEntry(key, value, i);
-                    return;
-                }
-            }
+for (Entry e = tab[i];
+     e != null;
+     e = tab[i = nextIndex(i, len)]) {
+    ThreadLocal<?> k = e.get();
+
+    if (k == key) {
+        e.value = value;
+        return;
+    }
+
+    if (k == null) {
+        replaceStaleEntry(key, value, i);
+        return;
+    }
+}
 ```
 ```
 3. Threadlocal 为弱引用;
 3. Threadlocal 为弱引用;
 
 
 ## Threadlocal 采用弱引用而不是强引用
 ## Threadlocal 采用弱引用而不是强引用
 
 
-1. 如果 key 是为强引用: 当我们现在将 ThreadLocal 的引用指向为 null,但是每个线程中有自己独立 ThreadLocalMap 还一直在继续持有该对象,但是我们ThreadLocal 对象不会被回收,就会发生 ThreadLocal 内存泄漏的问题。
+1. 如果 key 是为强引用: 当我们现在将 ThreadLocal 的引用指向为 null,但是每个线程中有自己独立 ThreadLocalMap 还一直在继续持有该对象,所以我们ThreadLocal 对象不会被回收,就会发生 ThreadLocal 内存泄漏的问题。
 2. 如果 key 是为弱引用:当我们现在将 ThreadLocal 的引用指向为 null,Entry 中的 key 指向为 null,但是下次调用 set 方法的时候,会根据判断如果 key 空的情况下,直接删除,避免了 Entry 发生内存泄漏的问题。
 2. 如果 key 是为弱引用:当我们现在将 ThreadLocal 的引用指向为 null,Entry 中的 key 指向为 null,但是下次调用 set 方法的时候,会根据判断如果 key 空的情况下,直接删除,避免了 Entry 发生内存泄漏的问题。
 3. 不管是用强引用还是弱引用都是会发生内存泄漏的问题。弱引用中不会发生 ThreadLocal 内存泄漏的问题。
 3. 不管是用强引用还是弱引用都是会发生内存泄漏的问题。弱引用中不会发生 ThreadLocal 内存泄漏的问题。
 4. 但是最终根本的原因 Threadlocal 内存泄漏的问题,产生于 ThreadLocalMap 与我们当前线程的生命周期一样,如果没有手动的删除的情况下,就有可能会发生内存泄漏的问题。
 4. 但是最终根本的原因 Threadlocal 内存泄漏的问题,产生于 ThreadLocalMap 与我们当前线程的生命周期一样,如果没有手动的删除的情况下,就有可能会发生内存泄漏的问题。

二進制
后端/Java/JAVA高阶/JUC编程/assets/image-20211101152903627.png


+ 137 - 0
后端/多数据源/springboot配置多数据源.md

@@ -0,0 +1,137 @@
+> [TOC]
+
+# 1、创建配置类,配置多个DataSource
+
+```java
+@Configuration
+public class DataSourceConfig {
+    @Bean
+    @ConfigurationProperties("spring.datasource.master")
+    public DataSource masterDataSource() {
+        return DruidDataSourceBuilder.create().build();
+    }
+
+    @Bean
+    @ConfigurationProperties("spring.datasource.slave01")
+    @ConditionalOnProperty(prefix = "spring.datasource.slave01", name = "enabled", havingValue = "true")
+    public DataSource slave01DataSource() {
+        return DruidDataSourceBuilder.create().build();
+    }
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource() {
+        DynamicDataSource dynamicDataSource = new DynamicDataSource();
+        //配置默认数据源
+        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
+        //配置多数据源
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER.getSourceName(), masterDataSource());
+        targetDataSources.put(DataSourceType.SLAVE01.getSourceName(), slave01DataSource());
+        dynamicDataSource.setTargetDataSources(targetDataSources);
+        return dynamicDataSource;
+    }
+}
+```
+
+
+
+# 2、配置yaml文件
+
+```yaml
+spring:
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    # 主库数据源
+    master:
+      driver-class-name: com.mysql.jdbc.Driver
+      url: jdbc:mysql://192.168.100.60:3306/demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
+      username: root
+      password: root
+    # 从库数据源
+    slave01:
+      enabled: true
+      driver-class-name: com.mysql.jdbc.Driver
+      url: jdbc:mysql://192.168.100.60:3306/demo01?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
+      username: root
+      password: root
+```
+
+# 3、继承AbstractRoutingDataSource类
+
+```java
+public class DynamicDataSource extends AbstractRoutingDataSource {
+    @Override
+    protected Object determineCurrentLookupKey() {
+        return DataSourceContextHolder.getDataSourceType();
+    }
+}
+```
+
+# 4、使用线程类切换数据源
+
+```java
+@Slf4j
+public class DataSourceContextHolder {
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源变量
+     * @param dataSourceType
+     */
+    public static void setDataSourceType(String dataSourceType){
+        log.info("切换到{}数据源", dataSourceType);
+        CONTEXT_HOLDER.set(dataSourceType);
+    }
+
+    /**
+     * 获取数据源变量
+     * @return
+     */
+    public static String getDataSourceType(){
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType(){
+        CONTEXT_HOLDER.remove();
+    }
+}
+```
+
+# 5、多数据源的使用
+
+```java
+public List<User> findAll() {
+    DataSourceContextHolder.setDataSourceType("slave01");
+    List<User> users = userDao.findAll();
+    DataSourceContextHolder.clearDataSourceType();
+    return users;
+}
+```
+
+# 6、DataSource源码
+
+```java
+protected DataSource determineTargetDataSource() {
+    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
+    // 根据determineCurrentLookupKey函数返回map的key
+    Object lookupKey = determineCurrentLookupKey();
+    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
+    // 如果找的到key对应的value则使用其他数据源,要不然使用默认的
+    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
+        dataSource = this.resolvedDefaultDataSource;
+    }
+    if (dataSource == null) {
+        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
+    }
+    return dataSource;
+}
+```
+

+ 197 - 0
后端/多数据源/事务导致多数据源切换失败.md

@@ -0,0 +1,197 @@
+> [TOC]
+
+# 1、使用注解形式配置多数据源
+
+```java
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface DBRouter {
+
+    /**
+     * 分库分表字段
+     */
+    String key() default "UId";
+
+    /**
+     * 是否分表
+     */
+    boolean splitTable() default false;
+
+}
+// 使用切面进行处理
+@Pointcut("@annotation(com.seamew.middleware.db.router.annotation.DBRouter)")
+public void aopPoint() {
+}
+
+@Around("aopPoint() && @annotation(dbRouter)")
+public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
+    String dbKey = dbRouter.key();
+    if (StringUtils.isBlank(dbKey)) {
+        throw new RuntimeException("annotation DBRouter key is null!");
+    }
+
+    // 路由属性
+    String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
+    // 路由策略
+    dbRouterStrategy.doRouter(dbKeyAttr);
+    // 返回结果
+    try {
+        return jp.proceed();
+    } finally {
+        dbRouterStrategy.clear();
+    }
+}
+```
+
+# 2、使用@Transactional
+
+在基于事务@Transactional情况下,当我们在执行事务方法时,会通过AOP机制先执行`DataSourceTransactionManager`的doBegin()方法,该方法进一步调用`AbstractRoutingDataSource`的getConnection()方法,再调用`determineCurrentLookupKey()`决定当前线程使用哪个数据源。
+
+* DataSourceTransactionManager
+
+```java
+@Override
+protected void doBegin(Object transaction, TransactionDefinition definition) {
+    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
+    Connection con = null;
+
+    try {
+        if (!txObject.hasConnectionHolder() ||
+            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
+            // 这里会获取connection
+            Connection newCon = obtainDataSource().getConnection();
+            // 忽略大部分代码 .....
+        }
+```
+
+* AbstractRoutingDataSource
+
+```java
+@Override
+public Connection getConnection() throws SQLException {
+    return determineTargetDataSource().getConnection();
+}
+// 最后调用这个determineTargetDataSource方法获取数据源
+// 需要注意此时并没有对自定义注解进行处理
+protected DataSource determineTargetDataSource() {
+    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
+    Object lookupKey = determineCurrentLookupKey();
+    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
+    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
+        dataSource = this.resolvedDefaultDataSource;
+    }
+    if (dataSource == null) {
+        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
+    }
+    return dataSource;
+}
+```
+
+# 3、造成失效原因
+
+如果使用事务mybatis会执行一下代码
+
+1. 获取sqlsession
+
+```java
+private class SqlSessionInterceptor implements InvocationHandler {
+    @Override
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+        // 获取sqlsession
+        SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
+                                              SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
+        try {
+            Object result = method.invoke(sqlSession, args);
+            if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
+                // force commit even on non-dirty sessions because some databases require
+                // a commit/rollback before calling close()
+                sqlSession.commit(true);
+            }
+            return result;
+```
+
+2. 反射执行方法
+
+```java
+@Override
+public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+    Statement stmt = null;
+    try {
+        // 这一步就是获取mysql connection
+        Configuration configuration = ms.getConfiguration();
+        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
+        stmt = prepareStatement(handler, ms.getStatementLog());
+        return handler.query(stmt, resultHandler);
+    } finally {
+        closeStatement(stmt);
+    }
+}
+```
+
+3. 获取sql connnection
+
+```java
+  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
+    Statement stmt;
+    // 直接获取缓存的connection
+    Connection connection = getConnection(statementLog);
+    stmt = handler.prepare(connection, transaction.getTimeout());
+    handler.parameterize(stmt);
+    return stmt;
+  }
+```
+
+**简单总结:**
+
+> 在开启事务后,便将默认连接放入缓存中,后续操作中直接复用了该连接,导致数据源切换失效。
+
+# 4、解决方法
+
+在事务开启前,实现数据源切换。改写决定数据源的核心方法`determineCurrentLookupKey`,先执行特定方法,将数据源放入`ThreadLocal`中,后续事务执行该方法中,从ThreadLocal中获取到要连接到数据源。
+
+这里用编程式事务举例:
+
+```java
+@Override
+protected Result grabActivity(PartakeReq partake, ActivityBillVO bill) {
+    try {
+        dbRouter.doRouter(partake.getUId());
+        return transactionTemplate.execute(status -> {
+            try {
+                // 执行sql 事务
+                insert();
+                updata();
+                return Result.buildResult(Constants.ResponseCode.INDEX_DUP);
+            }
+            return Result.buildSuccessResult();
+        });
+    } finally {
+        // 注意清理ThreadLocal缓存,不然会导致内存泄漏
+        dbRouter.clear();
+    }
+}
+```
+
+```java
+@Override
+public void doRouter(String dbKeyAttr) {
+    int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
+
+    // 扰动函数;在 JDK 的 HashMap 中,对于一个元素的存放,需要进行哈希散列。而为了让散列更加均匀,所以添加了扰动函数。
+    int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
+
+    // 库表索引;相当于是把一个长条的桶,切割成段,对应分库分表中的库编号和表编号
+    int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
+    int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
+
+    // 设置到 ThreadLocal
+    DBContextHolder.setDBKey(String.format("%02d", dbIdx));
+    DBContextHolder.setTBKey(String.format("%03d", tbIdx));
+    log.debug("数据库路由 dbIdx:{} tbIdx:{}",  dbIdx, tbIdx);
+}
+```
+
+# 5、参考文章
+
+[注意清理ThreadLocal缓存](../Java/JAVA高阶/JUC编程/Threadlocal.md)