一、简要概述

在做项目的时候遇到需要从两个数据源获取数据,项目使用的Spring + Mybatis环境,看到网上有一些关于多数据源的配置,自己也整理学习一下,然后自动切换实现从不同的数据源获取数据功能。

二、代码详解

2.1 DataSourceConstants 数据源常量类

  1. /**
  2. * 数据源名称常量类
  3. * 对应 application.xml 中 bean multipleDataSource
  4. * @author:dufy
  5. * @version:1.0.0
  6. * @date 2018/12/17
  7. */
  8. public class DataSourceConstants {
  9. /**
  10. * 数据源1,默认数据源配置
  11. */
  12. public static final String DATASOURCE_1 = "dataSource1";
  13. /**
  14. * 数据源2
  15. */
  16. public static final String DATASOURCE_2 = "dataSource2";
  17. }

2.2 DataSourceType 自定义数据源注解

  1. /**
  2. * 自定义数据源类型注解
  3. *
  4. * @author:dufy
  5. * @version:1.0.0
  6. * @date 2018/12/17
  7. */
  8. @Target(ElementType.TYPE)
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Documented
  11. public @interface DataSourceType {
  12. String value() default DataSourceConstants.DATASOURCE_1;
  13. }

2.3 MultipleDataSource 多数据源配置类

MultipleDataSource 继承 AbstractRoutingDataSource 类,为什么继承这个类就可以了?请看 第五章 :实现原理。

  1. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  2. /**
  3. * 自定义多数据源配置类
  4. *
  5. * @author:dufy
  6. * @version:1.0.0
  7. * @date 2018/12/17
  8. */
  9. public class MultipleDataSource extends AbstractRoutingDataSource {
  10. private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<String>();
  11. /**
  12. * 设置数据源
  13. * @param dataSource 数据源名称
  14. */
  15. public static void setDataSource(String dataSource){
  16. dataSourceHolder.set(dataSource);
  17. }
  18. /**
  19. * 获取数据源
  20. * @return
  21. */
  22. public static String getDatasource() {
  23. return dataSourceHolder.get();
  24. }
  25. /**
  26. * 清除数据源
  27. */
  28. public static void clearDataSource(){
  29. dataSourceHolder.remove();
  30. }
  31. @Override
  32. protected Object determineCurrentLookupKey() {
  33. return dataSourceHolder.get();
  34. }
  35. }

2.4 MultipleDataSourceAop 多数据源自动切换通知类

注意:请设置 @Order(0)。否则可能出现 数据源切换失败问题! 因为要在事务开启之前就进行判断,并进行切换数据源!

  1. /**
  2. * 多数据源自动切换通知类<br>
  3. * <p>
  4. * 首先判断当前类是否被该DataSourceType注解进行注释,如果没有指定注解,则采用默认的数据源配置; <br>
  5. * 如果有,则读取注解中的value值,将数据源切到value指定的数据源
  6. *
  7. * @author:dufy
  8. * @version:1.0.0
  9. * @date 2018/12/17
  10. */
  11. @Aspect // for aop
  12. @Component // for auto scan
  13. @Order(0) // execute before @Transactional
  14. public class MultipleDataSourceAop {
  15. private final Logger logger = Logger.getLogger(MultipleDataSourceAop.class);
  16. /**
  17. * 拦截 com.**.servicee中所有的方法,根据配置情况进行数据源切换
  18. * com.jiuling.tz.service
  19. * com.jiuling.web.service
  20. * @param joinPoint
  21. * @throws Throwable
  22. */
  23. @Before("execution(* com.dufy.*.service.*.*(..))")
  24. public void changeDataSource(JoinPoint joinPoint) throws Throwable {
  25. try {
  26. // 拦截的实体类,就是当前正在执行的service
  27. Class<?> clazz = joinPoint.getTarget().getClass();
  28. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  29. Method method = signature.getMethod();
  30. // 提取目标对象方法注解和类型注解中的数据源标识
  31. Class<?>[] types = method.getParameterTypes();
  32. if (clazz.isAnnotationPresent(DataSourceType.class)) {
  33. DataSourceType source = clazz.getAnnotation(DataSourceType.class);
  34. MultipleDataSource.setDataSource(source.value());
  35. logger.info("Service Class 数据源切换至--->" + source.value());
  36. }
  37. Method m = clazz.getMethod(method.getName(), types);
  38. if (m != null && m.isAnnotationPresent(DataSourceType.class)) {
  39. DataSourceType source = m.getAnnotation(DataSourceType.class);
  40. MultipleDataSource.setDataSource(source.value());
  41. logger.info("Service Method 数据源切换至--->" + source.value());
  42. }
  43. } catch (Exception e) {
  44. e.printStackTrace();
  45. }
  46. }
  47. /**
  48. * 方法结束后
  49. */
  50. @After("execution(* com.dufy.*.service.*.*(..))")
  51. public void afterReturning() throws Throwable {
  52. try {
  53. MultipleDataSource.clearDataSource();
  54. logger.debug("数据源已移除!");
  55. } catch (Exception e) {
  56. e.printStackTrace();
  57. logger.debug("数据源移除报错!");
  58. }
  59. }
  60. }

三、配置详情

applicationContext.xml 中配置详情

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
  4. xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"
  5. xmlns:util="http://www.springframework.org/schema/util" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  6. xmlns:cache="http://www.springframework.org/schema/cache"
  7. xsi:schemaLocation="
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context.xsd
  10. http://www.springframework.org/schema/beans
  11. http://www.springframework.org/schema/beans/spring-beans.xsd
  12. http://www.springframework.org/schema/tx
  13. http://www.springframework.org/schema/tx/spring-tx.xsd
  14. http://www.springframework.org/schema/jdbc
  15. http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
  16. http://www.springframework.org/schema/cache
  17. http://www.springframework.org/schema/cache/spring-cache.xsd
  18. http://www.springframework.org/schema/aop
  19. http://www.springframework.org/schema/aop/spring-aop.xsd
  20. http://www.springframework.org/schema/util
  21. http://www.springframework.org/schema/util/spring-util.xsd">
  22. <!-- 自动扫描包 ,将带有注解的类 纳入spring容器管理 -->
  23. <context:component-scan base-package="com.dufy"></context:component-scan>
  24. <!-- 引入配置文件 -->
  25. <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  26. <property name="locations">
  27. <list>
  28. <value>classpath*:jdbc.properties</value>
  29. </list>
  30. </property>
  31. </bean>
  32. <!-- dataSource1 配置 -->
  33. <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
  34. <!-- 基本属性 url、user、password -->
  35. <property name="url" value="${jdbc.url}"/>
  36. <property name="username" value="${jdbc.username}"/>
  37. <property name="password" value="${jdbc.password}"/>
  38. <!-- 配置初始化大小、最小、最大 -->
  39. <property name="initialSize" value="${ds.initialSize}"/>
  40. <property name="minIdle" value="${ds.minIdle}"/>
  41. <property name="maxActive" value="${ds.maxActive}"/>
  42. <!-- 配置获取连接等待超时的时间 -->
  43. <property name="maxWait" value="${ds.maxWait}"/>
  44. <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
  45. <property name="timeBetweenEvictionRunsMillis" value="${ds.timeBetweenEvictionRunsMillis}"/>
  46. <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
  47. <property name="minEvictableIdleTimeMillis" value="${ds.minEvictableIdleTimeMillis}"/>
  48. <property name="validationQuery" value="SELECT 'x'"/>
  49. <property name="testWhileIdle" value="true"/>
  50. <property name="testOnBorrow" value="false"/>
  51. <property name="testOnReturn" value="false"/>
  52. <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
  53. <property name="poolPreparedStatements" value="false"/>
  54. <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
  55. <!-- 配置监控统计拦截的filters -->
  56. <property name="filters" value="stat"/>
  57. </bean>
  58. <!-- dataSource2 配置-->
  59. <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
  60. <!-- 基本属性 url、user、password -->
  61. <property name="url" value="${jd.jdbc.url}"/>
  62. <property name="username" value="${jd.jdbc.username}"/>
  63. <property name="password" value="${jd.jdbc.password}"/>
  64. <!-- 其他配置省略 -->
  65. </bean>
  66. <!--多数据源配置-->
  67. <bean id="multipleDataSource" class="com.jiuling.core.ds.MultipleDataSource">
  68. <property name="defaultTargetDataSource" ref="dataSource1" />
  69. <property name="targetDataSources">
  70. <map key-type = "java.lang.String">
  71. <entry key="dataSource1" value-ref="dataSource1"/>
  72. <entry key="dataSource2" value-ref="dataSource2"/>
  73. <!-- 这里还可以加多个dataSource -->
  74. </map>
  75. </property>
  76. </bean>
  77. <!-- mybatis文件配置,扫描所有mapper文件 -->
  78. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="multipleDataSource"
  79. p:configLocation="classpath:mybatis-config.xml"
  80. p:mapperLocations="classpath:com/dufy/*/dao/*.xml"/>
  81. <!-- spring与mybatis整合配置,扫描所有dao -->
  82. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="com.dufy.*.dao"
  83. p:sqlSessionFactoryBeanName="sqlSessionFactory"/>
  84. <!-- 对dataSource 数据源进行事务管理 -->
  85. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
  86. p:dataSource-ref="multipleDataSource"/>
  87. <!-- 事务管理 通知 -->
  88. <tx:advice id="txAdvice" transaction-manager="transactionManager">
  89. <tx:attributes>
  90. <!-- 对insert,update,delete 开头的方法进行事务管理,只要有异常就回滚 -->
  91. <tx:method name="insert*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
  92. <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
  93. <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
  94. <!-- select,count开头的方法,开启只读,提高数据库访问性能 -->
  95. <tx:method name="select*" read-only="true"/>
  96. <tx:method name="count*" read-only="true"/>
  97. <!-- 对其他方法 使用默认的事务管理 -->
  98. <tx:method name="*"/>
  99. </tx:attributes>
  100. </tx:advice>
  101. <!-- 事务 aop 配置 -->
  102. <aop:config>
  103. <aop:pointcut id="serviceMethods" expression="execution(* com.dufy.*.service..*(..))"/>
  104. <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
  105. </aop:config>
  106. <!-- 配置使Spring采用CGLIB代理 -->
  107. <aop:aspectj-autoproxy proxy-target-class="true"/>
  108. <!-- 启用对事务注解的支持 -->
  109. <tx:annotation-driven transaction-manager="transactionManager"/>
  110. </beans>

jdbc.properties 配置内容

  1. ##-------------mysql数据库连接配置 ---------------------###
  2. # dataSource1
  3. jdbc.driver=com.mysql.jdbc.Driver
  4. jdbc.url=jdbc:mysql://192.168.1.110:3306/jdsc?useUnicode=true&characterEncoding=utf-8
  5. jdbc.username=test
  6. jdbc.password=123456
  7. #配置初始化大小、最小、最大
  8. ds.initialSize=1
  9. ds.minIdle=1
  10. ds.maxActive=20
  11. #配置获取连接等待超时的时间
  12. ds.maxWait=60000
  13. #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  14. ds.timeBetweenEvictionRunsMillis=60000
  15. #配置一个连接在池中最小生存的时间,单位是毫秒
  16. ds.minEvictableIdleTimeMillis=300000
  17. # dataSource2
  18. jd.jdbc.url=jdbc:mysql://192.168.1.120:3306/jdsc?useUnicode=true&characterEncoding=utf-8
  19. jd.jdbc.username=root
  20. jd.jdbc.password=123456

四、测试切换

注意:测试服务类的包路径,因为只有被AOP拦截的到的指定的Service才会进行数据源的切换。

  1. package com.dufy.web.service.impl.Data1ServiceImpl;
  2. /**
  3. * 使用 dataSourc1 ,配置dataSourc1的数据源
  4. * @author:duf
  5. * @version:1.0.0
  6. * @date 2018/12/17
  7. */
  8. @Service
  9. @DataSourceType(value = DataSourceConstants.DATASOURCE_1)
  10. public class Data1ServiceImpl implements Data1Service {
  11. @Resource
  12. private Data1Mapper data1Mapper;
  13. @Override
  14. public List<String> selectCaseByUpdateTime(String name) {
  15. List<String> data1 = data1Mapper.selectData1(name);
  16. return data1;
  17. }
  18. }
  19. package com.dufy.web.service.impl.Data2ServiceImpl;
  20. /**
  21. * 使用 dataSourc2 ,配置dataSourc2的数据源
  22. * @author:duf
  23. * @version:1.0.0
  24. * @date 2018/12/17
  25. */
  26. @Service
  27. @DataSourceType(value = DataSourceConstants.DATASOURCE_2)
  28. public class Data2ServiceImpl implements Data2Service {
  29. @Resource
  30. private Data2Mapper data2Mapper;
  31. @Override
  32. public List<String> selectCaseByUpdateTime(String name) {
  33. List<String> data2 = data2Mapper.selectData2(name);
  34. return data2;
  35. }
  36. }

通过测试后发现,两个Service服务器分别调用自己的配置的数据源进行数据的获取!

五、实现原理

基于AbstractRoutingDataSource 实现 多数据源配置,通过AOP来进行数据源的灵活切换。AOP的相关原理这里不做说明,就简单说一下 AbstractRoutingDataSource ,它是如何切换数据源的!

首先我们继承 AbstractRoutingDataSource 它是一个抽象类,然后要实现它里面的一个抽象方法。

  1. public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean

实现了InitializingBean,InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。

AbstractRoutingDataSource afterPropertiesSet方法的实现。

  1. public void afterPropertiesSet() {
  2. if (this.targetDataSources == null) {
  3. throw new IllegalArgumentException("Property 'targetDataSources' is required");
  4. } else {
  5. this.resolvedDataSources = new HashMap(this.targetDataSources.size());
  6. Iterator var1 = this.targetDataSources.entrySet().iterator();
  7. while(var1.hasNext()) {
  8. Entry<Object, Object> entry = (Entry)var1.next();
  9. Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
  10. DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
  11. this.resolvedDataSources.put(lookupKey, dataSource);
  12. }
  13. if (this.defaultTargetDataSource != null) {
  14. this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
  15. }
  16. }
  17. }

afterPropertiesSet方法将配置在 applicationContext.xml中的 targetDataSources 解析并构造出一个HashMap。

然后在实际过程中当需要访问数据库的时候,会首先获取一个Connection,下面看一下获取 Connection 的方法。

  1. public Connection getConnection() throws SQLException {
  2. return this.determineTargetDataSource().getConnection();
  3. }
  4. public Connection getConnection(String username, String password) throws SQLException {
  5. return this.determineTargetDataSource().getConnection(username, password);
  6. }
  7. protected DataSource determineTargetDataSource() {
  8. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  9. // 这里需要注意,通过子类的 determineCurrentLookupKey 方法 获取 lookupKey
  10. Object lookupKey = this.determineCurrentLookupKey();
  11. // 转换为对应的 DataSource 数据源
  12. DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
  13. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  14. // 如果符合上述条件,则获取默认的数据源,也就是在 ApplicationContext.xml 配置的默认数据源
  15. dataSource = this.resolvedDefaultDataSource;
  16. }
  17. if (dataSource == null) {
  18. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  19. } else {
  20. return dataSource;
  21. }
  22. }
  23. // 抽象方法,用于子类实现
  24. protected abstract Object determineCurrentLookupKey();

知道了 AbstractRoutingDataSource 的抽象方法后,通过AOP拦截,将Service上面配置不同的数据源进行装配到当前请求的ThreadLocal中, 最后 在获取Connection的时候,就能通过determineCurrentLookupKey 方法获取到 设置的数据源。

  1. @Override
  2. protected Object determineCurrentLookupKey() {
  3. return dataSourceHolder.get();
  4. }

再次强调必须保证切换数据源的Aspect必须在@Transactional这个Aspect之前执行,使用@Order(0)来保证切换数据源先于@Transactional执行)

六、参考文章

Spring, MyBatis 多数据源的配置和管理

spring配置多数据源涉及事务无法切换解决方案(绝对有效)


如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到,谢谢!

如果帅气(美丽)、睿智(聪颖),和我一样简单善良的你看到本篇博文中存在问题,请指出,我虚心接受你让我成长的批评,谢谢阅读!
祝你今天开心愉快!


欢迎访问我的csdn博客和关注的个人微信公众号!

愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人。

不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

博客首页 : http://blog.csdn.net/u010648555

© 每天都在变得更好的阿飞

[教程] Spring+Mybatis环境配置多数据源的更多相关文章

  1. idea spring+springmvc+mybatis环境配置整合详解

    idea spring+springmvc+mybatis环境配置整合详解 1.配置整合前所需准备的环境: 1.1:jdk1.8 1.2:idea2017.1.5 1.3:Maven 3.5.2 2. ...

  2. SpringBoot系列七:SpringBoot 整合 MyBatis(配置 druid 数据源、配置 MyBatis、事务控制、druid 监控)

    1.概念:SpringBoot 整合 MyBatis 2.背景 SpringBoot 得到最终效果是一个简化到极致的 WEB 开发,但是只要牵扯到 WEB 开发,就绝对不可能缺少数据层操作,所有的开发 ...

  3. spring boot 环境配置(profile)切换

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  4. spring+mybatis管理多个数据源(非分布式事务)

    本文通过一个demo,介绍如何使用spring+mybatis管理多个数据源,注意,本文的事务管理并非之前博文介绍的分布式事务. 这个demo将使用两个事务管理器分别管理两个数据源.对于每一个独立的事 ...

  5. 【Mybatis】MyBatis之配置自定义数据源(十一)

    本例是在[Mybatis]MyBatis之配置多数据源(十)的基础上进行拓展,查看本例请先学习第十章 实现原理 1.扩展Spring的AbstractRoutingDataSource抽象类(该类充当 ...

  6. Xamarin Anroid开发教程之验证环境配置是否正确

    Xamarin Anroid开发教程之验证环境配置是否正确 经过前面几节的内容已经把所有的编程环境设置完成了,但是如何才能确定所有的一切都处理争取并且没有任何错误呢?这就需要使用相应的实例来验证,本节 ...

  7. Spring Boot + MyBatis + Pagehelper 配置多数据源

    前言: 本文为springboot结合mybatis配置多数据源,在项目当中很多情况是使用主从数据源来读写分离,还有就是操作多库,本文介绍如何一个项目同时使用2个数据源. 也希望大家带着思考去学习!博 ...

  8. 【Mybatis】MyBatis之配置多数据源(十)

    在做项目的过程中,有时候一个数据源是不够,那么就需要配置多个数据源.本例介绍mybatis多数据源配置 前言 一般项目单数据源,使用流程如下: 单个数据源绑定给sessionFactory,再在Dao ...

  9. springmvc+spring+mybatis 项目配置

    前提 工作环境:JDK 1.8.Mysql 5.7.18.Intellij IDEA 2018.1.Tomcat 8.5.Maven 框架版本:Spring 4.2.0.RELEASE.SpringM ...

随机推荐

  1. 神奇:java中float,double,int的值比较运算

    float x = 302.01f;    System.out.println(x == 302.01); //false  System.out.println(x == 302.01f); // ...

  2. SQL记录-PLSQL面向对象

    PL/SQL面向对象 PL/SQL允许定义一个对象类型,这有助于在Oracle的数据库中设计的面向对象.对象类型可以包装复合类型.使用对象允许实现数据的具体结构现实世界中的对象和方法操作它.对象有属性 ...

  3. mybatis mapper接口开发dao层

    本文将探讨使用 mapper接口,以及 pojo 包装类进行 dao 层基本开发 mybatis dao 层开发只写 mapper 接口 其中需要 开发的接口实现一些开发规范 1. UserMappe ...

  4. 【原创】javascript模板引擎的简单实现

    本来想把之前对artTemplate源码解析的注释放上来分享下,不过隔了一年,找不到了,只好把当时分析模板引擎原理后,自己尝试 写下的模板引擎与大家分享下,留个纪念,记得当时还对比了好几个模板引擎来着 ...

  5. ASP.NET中异常处理的注意事项

    一.ASP.NET中需要引发异常的四类情况 1.如果运行代码后,造成内存泄漏.资源不可用或应用程序状态不可恢复,则引发异常.Console这个类中,有很多类似这样的代码: if ((value < ...

  6. 20155306 2016-2017-2 《Java程序设计》第5周学习总结

    20155306 2016-2017-2 <Java程序设计>第5周学习总结 教材学习内容总结 第八章 异常处理 8.1 语法与继承架构 Java中所有错误都会被打包为对象,运用try.c ...

  7. 第13月第10天 swift3.0

    1. Type 'Any' has no subscript members 这一条简直莫名其妙.大体意思就是,你这个类型"Any"不是个数组或者字典,不能按照下标取东西. 我之前 ...

  8. 3.微信公众号开发:配置与微信公众平台服务器交互的URL接口地址

    微信开发基本原理: 1.首先有3个对象 分别是微信用户端 微信公众平台服务器 开发者服务器(也就是放自己代码的服务器) 三者间互相交互 2.微信公众平台服务器 充当中间者角色 (以被动回复消息为例) ...

  9. fish(自动推荐命令;语法高亮等)

    Fish 是 Linux/Unix/Mac OS 的一个命令行 shell,有一些很好用的功能. 自动推荐 VGA 颜色 完美的脚本支持 基于网页的配置 帮助文档自动补全 语法高亮 以及更多 自动推荐 ...

  10. Linux使用一个定时器实现设置任意数量定时器功能【转】

    转自:https://www.jb51.net/article/120748.htm 为什么需要这个功能,因为大多数计算机软件时钟系统通常只能有一个时钟触发一次中断.当运行多个任务时,我们会想要多个定 ...