从mybatis的缓存到ThreadLocal的一点理解

本文详细解析MyBatis中的一级与二级缓存机制,以及如何通过线程封闭技术确保多线程环境下数据库连接的安全性。强调了正确配置缓存和合理使用ThreadLocal的重要性。

以下内容 spring已经实现了,这只是我的理解

一级缓存sqlsession,二级缓存factory

mybatis默认开启sqlsession缓存。每一个用户一个sqlsession,即使是共有的信息也会存到自己的缓存中,其他的用户查,即使是同样的方法,同一份数据,也会重新查数据库,因为sqlsession对象不一样。

//获取Mybatis对象
InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory factory=new SqlSessionFactoryBuilder().build(is);
SqlSession ss=factory.openSession();

所有的的sqlsession对象都是factory.openSession()得到的。所以是不是只要开启factory缓存即可呢?

开启factory缓存
在mapper中配置 标签catche

<cache readOnly="true"></cache>

注意,sqlsession查询公有数据的对象,只有在commit或者close的时候,才会把缓存刷新到factory中。

其他用户查数据的时候,先在自己的sqlsession中找,没有再去factory中找。都没有再查数据库,然后先自己缓存一份,提交或者关闭的时候,再刷新到factory中。

接下来是重点:

能走缓存的前提是,这些sqlsession对象都是同factory创建的。
两个线程,即使是执行相同的代码,也是不同的factory,还是没有办法走factory缓存。所以要保证,即使是不同的线程,factory也必须是一样的。并且每个线程都不是一个sqlsession对象,因为如果是一个,运行结束直接提交了。

所以必须是一个factory,不同的sqlsession。

一个线程一个sqlsession,并且每个线程只能有一个sqlsession,因为有可能调用不同的方法,所以要求任意执行位置获取sqlsession,都是唯一一个sqlsession。

问题1 :不同线程同一个factory

问题2 :不同线程,不同的sqlsession,并且每个线程只能有一个sqlsession。

解决:问题1
设置为静态的属性,只有1个factory。

解决:问题2
sqlsession不能作为静态属性,因为不同线程就是同一sqlsession了。
如果不是静态,那么即使是同一线程,调用两次,就是两个sqlsession了。怎么办呢?

解决:使用线程封闭

threadLocal 封装sqlsession对象。
threadLocal存储的数据,只能是线程内有效。

因为:
(线程1获取ThreadLocal对象,并在该对象中存储了数据B,那么在线程1中获取同一个ThreadLocal对象就可以获取到数据B,线程2获取线程1的ThreadLocal对象,但是无法获取线程1在ThreadLocal中存储的数据。)

三种封闭:

线程封闭:ad-hoc:程序控制,最糟糕,忽略。
堆栈封闭,局部变量,无并发问题。
threadLocal线程封闭:特别好。

线程封闭:

实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢?

就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。

1:ad-hoc线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。Ad-hoc线程封闭非常脆弱,没有任何一种语言特性能将对象封闭到目标线程上。
2:栈封闭
栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。
3:ThreadLocal封闭
使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

Threadlocal

就是在用户线程到达控制层之前就先将线程的数据封存到Threadlocal中,这样在以后需要用户信息的时候,直接在Threadlocal中取出来就可以了。如果不这样做,用户的信息在request中,我们就要把用户的信息不停的从controller往下传,可能还会传到util类中去。代码看起来也不舒服。使用过滤器和Threadlocal,就可以先把数据取出来,什么时候用,什么时候从Threadlocal中取就行了。

经典的线程封闭 数据库连接对应JDBC的 connection对象,将这个对象封闭在线程里边,虽然它本身不是线程安全的,但是通过线程封闭,也做到了线程安全。

public class MyBatisUtil {
	//声明Factory对象
	private static SqlSessionFactory factory;
	//声明ThreadLocal对象封装SqlSession对象
	private static ThreadLocal<SqlSession> ts=new ThreadLocal<>();
	static{
		InputStream is;
		try {
			is = Resources.getResourceAsStream("mybatis.xml");
			 factory=new SqlSessionFactoryBuilder().build(is);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}	
	//获取SqlSession对象
	//一个线程,第一次获取,没有,生成一份,以后不管在任意位置,再去调用,
	//都是之前那一份,注意,key是当前线程,所以肯定不会出现线程安全问题。
	public static SqlSession getSqlSession(){
		SqlSession ss=ts.get();
		if(ss==null){
			ss=factory.openSession();
			ts.set(ss);
		}
		return ss;		
	}	
	//关闭SqlSession对象
	public static void closeSqlSession(){
		SqlSession ss=ts.get();
		if(ss!=null){
			ss.close();
		}
		ts.set(null);		
	}		
}

注意:一定要在线程里进行关闭,因为你线程在外边关闭,我的sqlsession对象还在,所以你再次获取的时候,他还是存在的,但是这个对象已经没有用了,你线程都关闭了,也不会再得到这个对象,这是个无用对象,所以要关闭,给他清除掉。

还有,如果在业务中调用了关闭或者提交方法,那么在调用的时候,本线程中就不是一个sqlsession对象了,所以用过滤器,进行统一的管理。在整个线程数据库操作完成后再关闭sqlsession和提交sqlsession。

不过以上spring都实现好了。

### ThreadLocal 缓存数据测试脚本的实现方式 在使用 `ThreadLocal` 时,编写测试脚本需要关注线程隔离特性以及缓存数据的正确性。以下是详细的实现方式和代码示例。 #### 1. ThreadLocal 的基本原理 每个线程都有一个 `ThreadLocalMap` 类型的 `threadLocals` 属性[^1],该属性相当于一个 Map,其中 key 是 `ThreadLocal` 本身,value 是我们设置的值。由于每个线程都拥有独立的 `ThreadLocalMap`,因此可以保证线程间的数据隔离。 #### 2. 缓存数据的正确处理方法 为了确保缓存数据的正确性,必须注意以下几点: - **初始化与销毁**:在使用 `ThreadLocal` 时,建议通过 `initialValue()` 方法提供默认值,并在适当的时候清理资源以避免内存泄漏。 - **线程池场景**:如果使用线程池,需特别小心,因为线程可能被复用,导致旧的缓存数据残留。 #### 3. 测试脚本的实现方式 以下是一个基于 `ThreadLocal` 缓存数据的测试脚本示例,展示了如何正确使用和验证其行为。 ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalTest { // 定义 ThreadLocal 变量 private static final ThreadLocal<String> threadLocalCache = ThreadLocal.withInitial(() -> "Default Value"); // 设置缓存值 public static void setCache(String value) { threadLocalCache.set(value); } // 获取缓存值 public static String getCache() { return threadLocalCache.get(); } // 清理缓存 public static void clearCache() { threadLocalCache.remove(); } public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(5); // 提交多个任务模拟多线程环境 for (int i = 0; i < 10; i++) { final int taskId = i; executorService.submit(() -> { // 每个线程设置不同的缓存值 setCache("Task-" + taskId); System.out.println("Thread: " + Thread.currentThread().getName() + ", Cache: " + getCache()); // 模拟工作逻辑 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 清理缓存 clearCache(); }); } // 关闭线程池 executorService.shutdown(); } } ``` #### 4. 测试脚本的关键点 - **初始化默认值**:通过 `ThreadLocal.withInitial()` 方法为每个线程提供默认值[^1]。 - **线程安全**:每个线程操作的是自己的 `ThreadLocal` 实例,不会影响其他线程。 - **资源清理**:调用 `remove()` 方法清除线程中的 `ThreadLocal` 数据,防止内存泄漏。 #### 5. 处理缓存数据的正确方法 结合引用内容[^2],缓存数据的正确处理方法包括: - **读取缓存**:优先从缓存中获取数据,若缓存中不存在,则从数据库加载并更新缓存。 - **同步更新**:当数据库中的数据发生变化时,同步更新缓存中的数据,确保一致性。 #### 6. 结合 MyBatis缓存机制 根据引用内容[^3],MyBatis 的一级缓存由 `StatementHandler` 实现,而二级缓存可以通过插件自定义。如果在使用 `ThreadLocal` 时结合 MyBatis 缓存,需注意以下几点: - 确保 `ThreadLocal` 的生命周期与当前线程一致。 - 在事务结束或线程复用前清理 `ThreadLocal` 数据。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值