Jelajahi Sumber

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

seamew 2 tahun lalu
induk
melakukan
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)

TEMPAT SAMPAH
BUG/后端/assets/image-20230223144613302.png


TEMPAT SAMPAH
BUG/后端/assets/image-20230223144726612.png


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

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

TEMPAT SAMPAH
后端/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)