探究MyBatis缓存机制的细节
第1章:引言
大家好,我是小黑。今天我们要聊的是MyBatis的缓存机制。作为Java开发中经常使用的持久层框架,MyBatis以其灵活性和简便性而广受欢迎。但你知道吗,很多时候,正是因为这些特点,我们需要更深入地理解它的内部工作原理,尤其是缓存机制。这不仅能帮助我们更高效地使用MyBatis,还能在出现问题时快速定位和解决。
缓存机制,简单来说,就是暂时存储数据的一种方式,以便于快速访问。在MyBatis中,它主要用于减少数据库的访问次数,提高查询效率。MyBatis提供了两级缓存:一级缓存和二级缓存。这两种缓存有不同的作用域和生命周期,理解它们的区别对于使用MyBatis至关重要。
第2章:MyBatis缓存概览
一般来说,MyBatis的缓存分为一级缓存和二级缓存。一级缓存是默认开启的,它基于SqlSession级别,也就是说,只在SqlSession开启和关闭之间有效。而二级缓存则是基于namespace级别的,这意味着它可以跨SqlSession共享数据。
当咱们执行一个数据库查询时,MyBatis首先会查看缓存中是否已经有这个查询的结果。如果有,直接从缓存中获取数据,这样就避免了对数据库的访问,极大地提高了效率。如果没有,它才会执行SQL语句,然后将结果存入缓存。
来,我们用个简单的例子来看看一级缓存是怎么工作的。假设咱们有一个查询用户信息的操作,代码大概是这样的:
// 创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 获取Mapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,结果会放在一级缓存中
User user = mapper.selectUserById(1);
// 再次查询同一个ID的用户
User sameUser = mapper.selectUserById(1);
} finally {
sqlSession.close();
}
在这个例子中,第二次查询同一个ID的用户时,MyBatis不会再去执行SQL语句,而是直接从一级缓存中获取数据。这个过程对用户是透明的,但背后却节省了大量的数据库访问时间。
二级缓存的工作原理类似,不过它是跨SqlSession的。这意味着,即使SqlSession关闭了,只要是同一个namespace下的SqlSession,都可以访问到这个缓存。不过,使用二级缓存需要一些额外的配置。
明白了这些,咱们在使用MyBatis时就能更加得心应手了。知道怎么样,通过合理的缓存策略,可以大大提高应用的性能。不过,记得缓存也不是万能的,不当的使用同样会带来问题,比如数据不一致性等。所以,合理配置和使用MyBatis的缓存机制,对于开发高效、稳定的Java应用来说是非常关键的。
接下来,咱们继续深入探讨MyBatis缓存的具体细节,看看它是怎样在幕后默默优化我们的数据访问的。了解这些原理,对于咱们解决实际开发中遇到的性能瓶颈和问题是大有裨益的。别小看这些幕后的英雄,它们往往能在关键时刻大显身手。
MyBatis的一级缓存和二级缓存虽然目的相同,都是为了减少数据库的访问,提高效率,但它们的作用范围和使用方式却大有不同。掌握它们的特性和适用场景,能让咱们更加灵活地处理各种数据访问需求。
第3章:一级缓存深度解析
一级缓存的工作原理
一级缓存,也称为本地缓存,它默认是开启的。它的作用域是SqlSession。这意味着,当咱们在一个SqlSession中执行查询操作时,MyBatis会将查询结果存储在这个SqlSession的缓存中。如果后续有相同的查询操作,MyBatis会直接从缓存中获取结果,而不是再次访问数据库。
来看看一级缓存的一个简单示例。假设小黑现在要查询一个用户的信息,代码大致如下:
// 创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 获取UserMapper接口
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 查询用户信息,ID为1
User user1 = mapper.selectUserById(1);
// 再次查询相同ID的用户
User user2 = mapper.selectUserById(1);
} finally {
sqlSession.close();
}
在这个例子中,user1和user2其实是同一个对象。当第一次查询用户信息时,MyBatis将结果存储在一级缓存中。第二次查询相同ID的用户时,MyBatis直接从一级缓存中获取数据,而不需要再次访问数据库。
一级缓存的生命周期和作用域
一级缓存的生命周期和SqlSession一致。当SqlSession结束或关闭时,与之关联的一级缓存也就不存在了。这也是为什么它被称为本地缓存的原因。它只对当前的SqlSession有效,不能跨SqlSession共享数据。
管理一级缓存
虽然一级缓存默认是开启的,但在某些情况下,咱们可能需要清空或绕过缓存。比如,当执行了INSERT、UPDATE或DELETE操作后,缓存中的数据可能就不再是最新的了。这时候,咱们可以手动清空缓存,以确保数据的一致性。
// 执行更新操作
mapper.updateUser(user);
// 手动清空一级缓存
sqlSession.clearCache();
在这个例子中,更新操作之后,我们调用了sqlSession.clearCache()
方法来清空缓存。这样做可以避免脏读,确保数据的准确性。
一级缓存是MyBatis为了提高数据处理效率而提供的一个特性。它在单个SqlSession的范围内有效,可以减少对数据库的访问次数。但同时,也需要注意它的生命周期和作用域,合理管理缓存,以避免数据不一致的问题。理解了这些,咱们在使用MyBatis时就能更加得心应手,有效提升数据处理的效率和准确性。
第4章:二级缓存深度解析
二级缓存的工作原理
二级缓存是基于namespace的。当多个SqlSession操作相同namespace的映射器(Mapper)时,它们可以共享同一个二级缓存区域。例如,如果多个SqlSession都使用了相同的UserMapper,那么它们就可以共享UserMapper的二级缓存。
在MyBatis配置文件中开启二级缓存是非常简单的。只需要在mybatis-config.xml
文件中添加如下配置:
<configuration>
<settings>
<!-- 开启全局二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
接下来,在Mapper映射文件中也需要进行配置:
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启这个Mapper的二级缓存 -->
<cache/>
<!-- 其他SQL映射语句 -->
</mapper>
在这里,我们通过<cache/>
标签开启了UserMapper的二级缓存。
使用二级缓存的步骤
使用二级缓存,需要先进行全局配置和Mapper级别的配置,接着就可以在实际的操作中体会到它带来的便利了。比如,当第一个SqlSession查询了某个用户的信息并关闭后,这个信息会被存储在二级缓存中。当另一个SqlSession再次查询相同的数据时,就可以直接从二级缓存中获取,而不必再次访问数据库。
这里用一个例子来说明:
// 第一个SqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
try {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次查询,会将数据存储在二级缓存中
User user1 = mapper1.selectUserById(1);
} finally {
sqlSession1.close();
}
// 第二个SqlSession
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次查询,会尝试从二级缓存中获取数据
User user2 = mapper2.selectUserById(1);
} finally {
sqlSession2.close();
}
二级缓存的作用域与生命周期
二级缓存的生命周期跟SqlSessionFactory一致。它开始于SqlSessionFactory被创建,结束于SqlSessionFactory被关闭。二级缓存的作用范围是整个SqlSessionFactory范围内的所有SqlSession,只要它们操作相同的Mapper接口。
第5章:缓存策略与实现
不同缓存策略介绍
在MyBatis中,常见的缓存策略有:先进先出(FIFO)、最近最少使用(LRU)、软引用(Soft)和弱引用(Weak)。每种策略都有其特点和适用场景。
- FIFO(First In First Out):这种策略是按照对象进入缓存的顺序来移除它们。最早进入的对象会最先被移除。
- LRU(Least Recently Used):最近最少使用的对象会被首先移除。这种策略是基于对象被访问的次数和频率,适用于大部分缓存场景。
- 软引用(Soft Reference):在这种策略下,对象会被封装在软引用中。当JVM内存不足时,这些对象可能会被垃圾回收器回收。
- 弱引用(Weak Reference):类似于软引用,但生命周期更短。在JVM进行垃圾回收时,这些对象更有可能被回收。
自定义缓存策略
MyBatis允许我们自定义缓存策略。这意味着我们可以根据具体的应用需求设计和实现自己的缓存逻辑。比如,我们可能需要一个复合策略,结合LRU和软引用。
在MyBatis中,自定义缓存策略需要实现org.apache.ibatis.cache.Cache
接口。这个接口包含了缓存操作所需的基本方法,如getObject
、putObject
、removeObject
等。
下面是一个简单的自定义缓存实现示例:
public class CustomCache implements Cache {
// 缓存标识符
private final String id;
public CustomCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
// 实现添加缓存逻辑
}
@Override
public Object getObject(Object key) {
// 实现获取缓存逻辑
return null;
}
@Override
public Object removeObject(Object key) {
// 实现移除缓存逻辑
return null;
}
@Override
public void clear() {
// 实现清空缓存逻辑
}
@Override
public int getSize() {
// 实现获取缓存大小逻辑
return 0;
}
}
在这个自定义缓存中,我们定义了缓存的基本操作。根据实际需求,可以在这些方法中实现具体的缓存策略。
实例分析:选择合适的缓存策略
选择合适的缓存策略对于提高应用性能至关重要。例如,对于读多写少的应用,LRU可能是一个不错的选择。而对于内存敏感的应用,使用软引用或弱引用策略可能更合适。
第6章:缓存失效与维护
缓存失效的场景
在MyBatis中,缓存失效主要发生在以下几种情况:
- 数据更新:当执行UPDATE、DELETE或INSERT操作时,与这些操作相关的缓存数据可能会变得过时。此时,为了保证数据的一致性,需要使缓存失效。
- SqlSession关闭:对于一级缓存来说,当SqlSession关闭或者提交时,缓存就会失效。
- 手动清除:我们可以通过编程的方式手动清除缓存。
缓存维护的最佳实践
为了保证数据的准确性和一致性,咱们需要采取一些措施来维护缓存。下面是一些最佳实践:
- 合理使用缓存:不是所有情况都适合使用缓存。比如,对于经常变动的数据,使用缓存可能会带来更多的问题。
- 更新数据时清除缓存:在进行数据更新操作时,及时清除或更新相关的缓存。
- 合理配置缓存大小:避免缓存占用过多内存,合理配置缓存大小和清除策略。
如何处理缓存并发问题
在并发环境下,缓存可能会引起一些问题,比如脏读或者不一致的情况。处理这些并发问题,需要我们在设计时就考虑周全。
举个例子,如果两个用户同时读取并更新同一个数据,就可能产生并发问题。在这种情况下,咱们可以使用乐观锁或悲观锁来控制。乐观锁通常是通过版本号来实现,而悲观锁则是直接锁定数据行。
// 乐观锁更新数据的例子
public void updateUser(User user) {
int version = user.getVersion();
user.setVersion(version + 1);
int result = mapper.updateUser(user);
if (result == 0) {
// 更新失败,数据可能已被其他用户修改
}
}
在这个例子中,我们通过增加版本号来实现乐观锁。如果在更新时发现版本号不匹配,就意味着数据可能已经被其他用户更新,此时可以进行相应的处理,比如重试或者提示用户。
第7章:性能优化与实践案例
通过缓存优化MyBatis性能
MyBatis的缓存机制,如果使用得当,可以显著提升应用的响应速度和处理能力。这里有几个要点需要注意:
- 合理选择缓存级别:根据应用的具体需求,决定是使用一级缓存、二级缓存,还是两者结合。
- 适当配置缓存参数:根据数据量和访问频率,调整缓存大小、清除策略等参数。
- 避免不必要的缓存:对于频繁变动的数据,使用缓存可能会带来更多问题而非好处。
实战案例分析:在不同场景下的缓存应用
让我们通过一个实际案例来看看如何在MyBatis中应用缓存。假设小黑正在开发一个电商平台,其中商品信息的读取操作非常频繁,但更新操作相对较少。
在这种情况下,合理使用MyBatis的二级缓存是一个不错的选择。首先,我们需要在MyBatis配置文件中启用二级缓存,然后在商品信息的Mapper映射文件中添加缓存配置。
<mapper namespace="com.example.mapper.ProductMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<!-- 其他SQL映射语句 -->
</mapper>
在这个配置中,eviction="LRU"
指定了使用最近最少使用的清除策略,flushInterval="60000"
表示缓存每60秒刷新一次
,size="512"
设置了缓存的大小,而readOnly="true"
表明缓存数据是只读的。
// 使用缓存查询商品信息的示例
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);
// 查询商品信息,ID为123
Product product = mapper.selectProductById(123);
// 后续相同ID的查询将直接从缓存中获取数据
} finally {
sqlSession.close();
}
在这个例子中,当第一次查询ID为123的商品信息时,查询结果会被缓存在二级缓存中。后续对同一商品的查询将直接从缓存中获取数据,从而减少数据库的访问次数,提高查询效率。
在实际应用中,还可以根据需要调整缓存的配置。比如,对于一些热门商品,可以将它们的信息缓存时间设置得更长一些;而对于那些不经常变动的数据,可以使用更大的缓存。
第8章:总结
本篇博客,咱们深入探讨了MyBatis的一级和二级缓存。一级缓存帮助我们在一个SqlSession内部减少对数据库的访问,而二级缓存则扩展了这种优化到多个SqlSession,甚至整个应用的范围。
我们还讨论了不同的缓存策略,如FIFO、LRU、软引用和弱引用,以及如何根据应用的需求选择合适的策略。通过案例,我们看到了缓存在实际应用中的威力,它可以显著提高性能,但同时也需要注意数据一致性和缓存维护的问题。
关于MyBatis缓存机制的深入分析就聊到这里。希望这些内容对大家有所帮助~
推荐阅读
-
Hadoop 整体框架和每个组件的工作机制、流程细节。
-
彻底了解浏览器的缓存机制
-
研究说明:PBRT II 中的辐照度缓存机制
-
研究说明:PBRT I 中的辐照度缓存机制
-
Retrofit2.0+okhttp3 缓存机制和遇到的问题
-
Hibernate 如何处理事务?请描述 Hibernate 中的事务管理。Hibernate 中的缓存机制是什么?如何配置和使用缓存?
-
阿里味 "的《Redis核心实践全彩手册》给你,还学不会转行--Redis基本是必考点。在 "阿里味 "的《Redis核心实战全彩手册》里,你还是学不会转行--Redis基本是必考点: - Redis 常见的性能问题有哪些?Redis 最常见的性能问题有哪些,如何解决?--性能相关 - Redis 缓存的雪崩、击落和穿透到底意味着什么?如何处理?--缓存相关 - Redis 主从集群有哪些常见问题?如何解决?--可用性 - 现有的 Redis 实例有 6GB 的存储空间,预计将来会扩展到 32GB,你能提供解决方案并分析其优势和潜在问题吗?--可扩展性相关 毕竟,10 家公司中至少有 8 家的架构系统中都有 Redis,基本上可以说是 IT 基础架构的必备系统。 因此,Redis 的开发和运维是很多大厂的重要工作,也是我们必须掌握的技术栈。 不过,Redis 毕竟是一个复杂的键值数据库,在实际使用中,有非常多的技术点需要注意,比如:各种数据结构、数据持久化机制、分片集群、主从集群等等。 一不小心,性能就会每况愈下,失去 "快 "的最大特点!
-
DAG 区块链项目 SPECTRE 的技术细节:围绕一致性建立投票机制,筛查攻击以消除交易冲突
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
标题:一文搞定Redis面试,附Redis面试大纲+常见Redis面试题-一、基础篇 快速上手 ①. 什么是redis ②. 为什么使用redis ③. 安装 ④. 基本使用(常见数据结构的命令) Java操作redis ①. Jedis ②. SpringBoot 启动redis的方式 ①. 配置文件 ②. 生产环境启动方案 二、进阶篇 redis实现session共享 redis缓存的使用 ①. 注解式 ②. Spring Cache 数据库和缓存双写一致性问题——穿透 redis实现附近的人 redis实现计数器 redis事务 redis分布式锁的使用 redis集群 redis实现延时队列 redis实现限流 redis实现布隆过滤器 发布订阅 redis优化 三、原理篇 redis单线程为什么性能好 数据类型的底层实现 持久化机制 过期策略 内存淘汰 redis优化 哨兵模