1. 创建工程2. 引入依赖<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <!-- Spring jdbc 使用的依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency> </dependencies>3. 准备代码实体类代码/** * 账户的实体类 */public class Account implements Serializable { //数据id private Integer id; //账号编码 private String accountNum; //账号金额 private Float money;}持久层接口代码/** * 账户的持久层接口 */public interface IAccountDao { /** * 根据Id查询账户 * @param accountId * @return */ Account findAccountById(Integer accountId); /** * 保存账户 * @param account */ void saveAccount(Account account); /** * 更新账户 * @param account */ void updateAccount(Account account);}持久层实现类代码/** * 账户的持久层实现类 */@Repositorypublic class AccountDaoImpl implements IAccountDao { //jdbc模板类属性 @Autowired private JdbcTemplate jdbcTemplate; //根据id查找 public Account findAccountById(Integer accountId) { List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId); return accounts.isEmpty()?null:accounts.get(0); } public void saveAccount(Account account) { jdbcTemplate.update("insert into account values(?,?,?)", account.getId(),account.getAccountNum(),account.getMoney()); } public void updateAccount(Account account) { jdbcTemplate.update("update account set accountnum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }}业务层接口代码/** * @Auther: wyan */public interface UserService { /** * 账户转账 * @param fromId toId */ public void transMoney(Integer fromId, Integer toId, Integer money);}业务层实现类代码/** * @Auther: wyan * @Description: */@Service@Transactionalpublic class UserServiceImpl implements UserService { @Autowired private IAccountDao accountDao; public void transMoney(Integer fromId, Integer toId, Integer money) { Account fromAccount = accountDao.findAccountById(fromId); Account toAccount = accountDao.findAccountById(toId); //原始账号减钱 fromAccount.setMoney(fromAccount.getMoney()-money); accountDao.updateAccount(fromAccount); //抛出异常 int i=1/0; //转账账号加钱 toAccount.setMoney(toAccount.getMoney()+money); accountDao.updateAccount(toAccount); }}Tips: 此时需要注意注解 @Transactional 的含义。Transactional 就是表示事务,那么在此类上面加入注解,说明需要 Spring 框架针对此类的方法做事务的增强行为,也就是说此注解其实是相当于我们在配置文件中配置的节点 tx:advice。那么这时候有的细心的同学可能会有些疑问:我们在 xml 文件中可以配置事务的传播行为与隔离级别,那么这一个注解如何制定事务的传播行为与隔离级别呢?一个类中如果定义方法过多,而实际上需要增强控制事务的方法只有一部分,如何缩小粒度,只控制需要事务的方法呢?ok,大家。这里有必要跟大家解释下此注解的其余使用方式:问题一答疑:在注解后面可以通过括号内的参数设置隔离级别与传播行为。比如:@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED) 此表达式的含义是事务一定需要,并且是读已提交。问题二答疑:在方法上使用注解。类上面可以不使用 @Transactional 注解,而是将注解写在需要用到事务的方法之上。4. 配置文件<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--配置JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置数据源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///transmoney"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--包路径扫描--> <context:component-scan base-package="com.offcn"></context:component-scan> <!--事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置数据源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///transmoney"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--包路径扫描--> <context:component-scan base-package="com.offcn"></context:component-scan> <!--事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--注解事务驱动--> <tx:annotation-driven/></beans>Tips: 此处需要注意 tx:annotation-driven 节点无需配置通知节点与切面节点,而是使用 tx:annotation-driven 节点表示,事务的支持方式为声明式事务。5. 测试代码public class AccountServiceTest { public static void main(String[] args) { //1.获取容器 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //2.获取业务对象 UserService userService = ac.getBean(UserService.class); //3.从id为1的账号转成1000到2账号 userService.transMoney(1,2,1000); System.out.println("转账完成.."); }}6. 测试结果:ok, 大家,我们继续测试之前的转账代码,依然得到错误的异常信息。同时数据库的金额并没有发生改变,因为事务的控制,保证了数据的一致性原子性。那么也证明我们声明式事务的案例测试成功。
1. 创建工程2. 引入依赖<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <!-- Spring jdbc 使用的依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency> </dependencies>3. 准备代码实体类代码/** * 账户的实体类 */public class Account implements Serializable { //数据id private Integer id; //账号编码 private String accountNum; //账号金额 private Float money;}持久层接口代码/** * 账户的持久层接口 */public interface IAccountDao { /** * 根据Id查询账户 * @param accountId * @return */ Account findAccountById(Integer accountId); /** * 保存账户 * @param account */ void saveAccount(Account account); /** * 更新账户 * @param account */ void updateAccount(Account account);}持久层实现类代码/** * 账户的持久层实现类 */@Repositorypublic class AccountDaoImpl implements IAccountDao { //jdbc模板类属性 @Autowired private JdbcTemplate jdbcTemplate; //根据id查找 public Account findAccountById(Integer accountId) { List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId); return accounts.isEmpty()?null:accounts.get(0); } public void saveAccount(Account account) { jdbcTemplate.update("insert into account values(?,?,?)", account.getId(),account.getAccountNum(),account.getMoney()); } public void updateAccount(Account account) { jdbcTemplate.update("update account set accountnum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }}业务层接口代码/** * @Auther: wyan */public interface UserService { /** * 账户转账 * @param fromId toId */ public void transMoney(Integer fromId, Integer toId, Integer money);}业务层实现类代码/** * @Auther: wyan * @Description: */@Servicepublic class UserServiceImpl implements UserService { @Autowired private IAccountDao accountDao; public void transMoney(Integer fromId, Integer toId, Integer money) { Account fromAccount = accountDao.findAccountById(fromId); Account toAccount = accountDao.findAccountById(toId); //原始账号减钱 fromAccount.setMoney(fromAccount.getMoney()-money); accountDao.updateAccount(fromAccount); //抛出异常 int i=1/0; //转账账号加钱 toAccount.setMoney(toAccount.getMoney()+money); accountDao.updateAccount(toAccount); }}Tips: 我们再给原始账号减掉钱后,执行保存。然后在这里会出现个异常,就是为了测试事务的特性,所以手动加了个除 0 的代码。4. 配置文件<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--配置JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置数据源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///transmoney"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--包路径扫描--> <context:component-scan base-package="com.offcn"></context:component-scan> <!--事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--事务的通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="save*" propagation="REQUIRED"/> <tx:method name="del*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="find*" read-only="true"/> </tx:attributes> </tx:advice> <!--切面的配置--> <aop:config> <aop:pointcut id="pt" expression="execution(* com.offcn.service.*.*(..))"/> <aop:advisor pointcut-ref="pt" advice-ref="txAdvice" /> </aop:config></beans>此处需要注意: context:component-scan:扫描的节点路径为包含 service 和 dao 两个子目录的父级目录; transactionManager: 此节点作用就是初始化 Spring 框架提供的事务管理器的实现类; tx:advice: 此节点的作用是配置切面的通知,因为之前我们的切面类是自定义的,这里使用的是 Spring 提供的事务管理器类作为切面,那么针对什么方法需要做增强呢,在此节点配置,可以看得出来:以 save、del、update 开头的方法都会支持事务,而 find 开头的方法,指定的是只读。 aop:config: 此节点就是 AOP 的相关配置节点了,将切入点和通知整合到一起,同以前的项目差别不大。这里可以看到:切入点规则是针对 service 下面的所有类所有方法任意参数做增强。通知使用的就是我们上面配置过的 tx:advice 节点。5. 测试代码public class AccountServiceTest { public static void main(String[] args) { //1.获取容器 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //2.获取业务对象 UserService userService = ac.getBean(UserService.class); //3.从id为1的账号转成1000到2账号 userService.transMoney(1,2,1000); System.out.println("转账完成.."); }}6. 测试结果:ok, 大家,控制台如愿以偿打印了异常的堆栈信息,但是这个不是目的,哈哈哈。目的是在程序执行发生异常的情况下,数据的数据不会错乱。我们可以看见数据库数据并没有发生改变。
在我们正式介绍什么是服务资源隔离之前,我们先来了解一些前置的概念,这些概念是理解服务资源隔离的前提。进程与线程进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在当代面向线程设计的计算机结构中,进程是线程的容器。我们可以把进程理解为我们项目运行的载体,就比如我们乘坐的公交车,公交车相对于我们来说就是一个载体,来承载我们到达不同的目的地,进程就是如此,只不过在进程中被承载的是线程罢了。线程:线程(Thread)是操作系统能够进行运算调度的最小单位。我们可以把线程理解为,执行某一具体的计算机系统任务的执行者,比如在公交车中,负责开公交车的司机师傅就可以被当做一个线程。在一个进程中,可以存在多个线程,即在一辆公交车中可以存在多名乘客,他们分别都去往不同的目的地,但是,一个线程只能属于一个进程,不属于不同的两个进程,即一名乘客同一时刻只能乘坐一辆公交车,不可能在同一时刻乘坐两辆公交车。在理解了什么是进程与线程之后,我们来看一下在我们的 Web 项目中,进程与线程都是怎样存在的,以及他们之间的关系是怎样的。Web 项目中的进程与线程我们的每一个 Web 项目都可以被看作一个进程,且一个 Web 项目只能是一个进程。在我们的 Web 项目中,常规情况下只有两个线程,分别是主线程和工作线程,其中,主线程负责我们的项目启动以及一些项目初始化工作,而工作线程则主要负责项目中的请求处理与业务逻辑执行,项目中进程与线程的关系如下图所示:Java Web 项目中进程与线程的关系根据上图,我们可以这样理解:一个 Web 项目在计算机系统中就是一个进程,而在这个进程中,存在一个主线程和一个工作线程,并且主线程主要负责项目启动,而工作线程主要负责请求的处理。在理解了这个关系之后,让我们来看一下什么是服务资源隔离。服务资源隔离在介绍服务资源隔离之前,我们需要先了解什么是服务资源。服务资源一般来讲,指的是项目正常运行所需要的基础环境、基础设施、静态资源文件等内容,而对于 Web 项目来说,其项目本身即是一种服务资源,服务调用者通过调用项目提供的服务来满足他们的业务需求。而对于服务提供者来说,这些业务需求的实现在项目中一般就是我们所开发的接口,所以,在项目中所实现的业务接口即是我们这里所说的服务资源。那么,为什么需要把服务资源进行隔离呢?我们知道,正常情况下,在 Web 项目中只有一个工作线程,且这个工作线程负责接口请求的处理。在正常应用场景下,服务调用者会调用我们项目所提供的接口来满足业务需求,这里假设我们的一个 Web 项目中具有 5 个接口,服务调用者会根据业务顺序来调用我们的接口,如果一切顺利,则业务即可正常顺利地进行下去。但是,如果服务调用者在调用接口时,其中一个接口所需要处理的业务比较复杂,导致这个接口不能及时的结束,这就导致我们后续的接口调用只能等待,直到该接口处理完毕后才能继续向下执行,如果该接口一直不能处理完毕,则后续接口就会一直等待,从而影响业务的正常开展,这种现象就被称为服务资源等待,如下图所示:服务资源等待产生原理我们可以把上图中的工作线程访问理解为服务调用者,在服务调用者调用接口 2 时,由于接口 2 迟迟不能处理,导致接口 2 出现服务等待,并最终影响后续的接口 3、接口 4、接口 5 的调用,从而影响了业务的顺利进行。如果通过采取某种措施,使满足同一业务需求的不同服务资源间进行隔离,来有效缓解或解决服务资源等待问题,那么业务就可以正常顺利地开展下去,所以人们就提出了服务资源隔离的概念。
1. 创建 maven 工程:pom 文件的 jar 包坐标如下:<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency></dependencies>2. 连接数据库的工具类:@Componentpublic class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); @Autowired private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * 获取当前线程上的连接 * @return */ public Connection getThreadConnection() { try{ //1.先从ThreadLocal上获取 Connection conn = tl.get(); //2.判断当前线程上是否有连接 if (conn == null) { //3.从数据源中获取一个连接,并且存入ThreadLocal中 conn = dataSource.getConnection(); conn.setAutoCommit(false); tl.set(conn); } //4.返回当前线程上的连接 return conn; }catch (Exception e){ throw new RuntimeException(e); } } /** * 把连接和线程解绑 */ public void removeConnection(){ tl.remove(); }}3. 实体类 Account:public class Account implements Serializable { //数据id private Integer id; //账号编码 private String accountNum; //账号金额 private Float money; //省略get 和set 方法}4. 持久层 dao 和 dao 的 实现类://dao的接口public interface IAccountDao { /** * 更新 * @param account */ void updateAccount(Account account); /** * 根据编号查询账户 */ Account findAccountByNum(String accountNum);}//dao的实现类@Repositorypublic class AccountDaoImpl implements IAccountDao { //dbutil的查询工具类 @Autowired private QueryRunner runner; //连接的工具类 @Autowired private ConnectionUtils connectionUtils; public void setRunner(QueryRunner runner) { this.runner = runner; } public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } //修改账号 public void updateAccount(Account account) { try{ runner.update(connectionUtils.getThreadConnection(),"update account set accountNum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }catch (Exception e) { throw new RuntimeException(e); } } //根据账号查询 public Account findAccountByNum(String accountNum) { try{ List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where accountNum = ? ",new BeanListHandler<Account>(Account.class),accountNum); if(accounts == null || accounts.size() == 0){ return null; } if(accounts.size() > 1){ throw new RuntimeException("结果集不唯一,数据有问题"); } return accounts.get(0); }catch (Exception e) { throw new RuntimeException(e); } }}代码解释: AccountDaoImpl 类上面的注解 @Repository 表示使用注解实例化此类,并交给 Spring 的容器管理。5. 业务类 Service 和 Service 的实现类://业务接口public interface IAccountService { /** * 转账 * @param sourceAccount 转出账户名称 * @param targetAccount 转入账户名称 * @param money 转账金额 */ void transfer(String sourceAccount, String targetAccount, Integer money);}//业务实现类@Servicepublic class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } public void transfer(String sourceAccount, String targetAccount, Integer money) { Account source = accountDao.findAccountByNum(sourceAccount); Account target = accountDao.findAccountByNum(targetAccount); source.setMoney(source.getMoney()-money); target.setMoney(target.getMoney()+money); accountDao.updateAccount(source); accountDao.updateAccount(target); System.out.println("转账完毕"); }}代码解释:AccountServiceImpl 类上面的注解 @Service 表示使用注解实例化此类,并交给 Spring 的容器管理。6. 事务管理器类@Component@Aspectpublic class TransactionManager { @Autowired private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } @Pointcut("execution(* com.offcn.service.impl.*.*(..))") private void pt1() {} /** * 开启事务 */ @Before("pt1()") public void beginTransaction(){ try { System.out.println("开启事务"); connectionUtils.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } /** * 提交事务 */ @AfterReturning("pt1()") public void commit(){ try { System.out.println("提交事务"); connectionUtils.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } /** * 回滚事务 */ @AfterThrowing("pt1()") public void rollback(){ try { System.out.println("回滚事务"); connectionUtils.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } /** * 释放连接 */ @After("pt1()") public void release(){ try { System.out.println("释放连接"); connectionUtils.getThreadConnection().close();//还回连接池中 connectionUtils.removeConnection(); }catch (Exception e){ e.printStackTrace(); } }}代码解释:此类通过注解 @Componet 实例化,并且交由 Spring 容器管理,@Aspect 表明它是一个切面类。而下面的注解 @Pointcut 和其余的方法上的各个通知注解,在上面也已经介绍过,这里不做赘述了。主要专注点在于每个注解的通知方法内部引入切入点的表达式方式。7. 配置文件:<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--连接数据库的必备信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/transmoney"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 注解扫描工程下的包路径--> <context:component-scan base-package="com.offcn"></context:component-scan> <!-- 注解代理模式 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>配置文件说明:dataSource: 采用 c3p0 数据源,大家一定要注意数据库的名称与账号名和密码;queryRunner: dbutils 第三方框架提供用于执行 sql 语句,操作数据库的一个工具类;context:component-scan: 此注解表示注解方式初始化容器扫描的包路径;aop:aspectj-autoproxy: 此注解表示开启代理模式8. 测试类代码@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:applicationContext.xml")public class AccountServiceTest { @Autowired private IAccountService accountService; @Test public void testTransfer(){ accountService.transfer("622200009999","622200001111",100); }}测试结果:执行代码后结果:可以看到,我们通过注解方式配置 Spring 的 AOP 相关配置,同样实现了对于数据的操作。
我们模拟一个实际生活中常见的情景,就是账号的转账。 假设有两个用户 A 和 用户 B,我们通过程序,从 A 账号中转成指定的 money 到 B 账号中。那么,针对正常和异常的程序执行,我们来分析下问题以及它的解决方案。2.1.1 工程准备创建 maven 工程引入 pom 文件的依赖 jar 包坐标信息<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency></dependencies>Spring 框架的配置文件编写<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 配置Service --> <bean id="accountService" class="com.offcn.service.impl.AccountServiceImpl"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> </bean> <!--配置Dao对象--> <bean id="accountDao" class="com.offcn.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="queryRunner"></property> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!--配置QueryRunner--> <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--连接数据库的必备信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/transmoney"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 配置Connection的工具类 ConnectionUtils --> <bean id="connectionUtils" class="com.offcn.utils.ConnectionUtils"> <!-- 注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean></beans>配置文件说明:connectionUtils 是获取数据库连接的工具类;dataSource 采用 c3p0 数据源,大家一定要注意数据库的名称与账号名和密码;queryRunner 是 dbutils 第三方框架提供用于执行 SQL 语句,操作数据库的一个工具类;accountDao 和 accountService 是我们自定义的业务层实现类和持久层实现类。项目使用数据库环境CREATE TABLE account (id int(11) NOT NULL auto_increment,accountNum varchar(20) default NULL,money int(8) default NULL,PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf82.1.2 代码编写实体类代码public class Account implements Serializable { //数据id private Integer id; //账号编码 private String accountNum; //账号金额 private Float money; //省略 get 和 set 的方法}持久层接口//接口代码public interface IAccountDao { /** * 更新 * @param account */ void updateAccount(Account account); /** * 根据编号查询账户 * @param accountNum * @return 如果有唯一的一个结果就返回,如果没有结果就返回null * 如果结果集超过一个就抛异常 */ Account findAccountByNum(String accountNum);}持久层实现类public class AccountDaoImpl implements IAccountDao { //数据库查询工具类 private QueryRunner runner; //数据库连接工具类 private ConnectionUtils connectionUtils; //省略 get 和 set 的方法 //修改账号的方法 public void updateAccount(Account account) { try{ runner.update(connectionUtils.getThreadConnection(), "update account set accountNum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }catch (Exception e) { throw new RuntimeException(e); } } //根据账号查询 Account 对象的方法 public Account findAccountByNum(String accountNum) { try{ List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "select * from account where accountNum = ? ",new BeanListHandler<Account>(Account.class),accountNum); if(accounts == null || accounts.size() == 0){ return null; } if(accounts.size() > 1){ throw new RuntimeException("结果集不唯一,数据有问题"); } return accounts.get(0); }catch (Exception e) { throw new RuntimeException(e); } }}业务层接口public interface IAccountService { /** * 转账 * @param sourceAccount 转出账户名称 * @param targetAccount 转入账户名称 * @param money 转账金额 */ void transfer(String sourceAccount, String targetAccount, Integer money);}业务层实现类public class AccountServiceImpl implements IAccountService { //持久层对象 private IAccountDao accountDao; //省略 set 和 get 方法 //转账的方法 public void transfer(String sourceAccount, String targetAccount, Integer money) { //查询原始账户 Account source = accountDao.findAccountByNum(sourceAccount); //查询目标账户 Account target = accountDao.findAccountByNum(targetAccount); //原始账号减钱 source.setMoney(source.getMoney()-money); //目标账号加钱 target.setMoney(target.getMoney()+money); //更新原始账号 accountDao.updateAccount(source); //更新目标账号 accountDao.updateAccount(target); System.out.println("转账完毕"); }}测试运行类代码@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:bean.xml")public class AccountServiceTest { @Autowired @Qualifier("proxyAccountService") private IAccountService as; @Test public void testTransfer(){ as.transfer("622200009999","622200001111",100); }}测试结果代码执行完毕,可以看到输出打印转账 ok 了。那么数据库的数据有没有改变呢?我们再看一眼:可以看到:两个账号的数据已经发生了改变,证明转账的动作,确实完成了。那这样看来,我们的代码也没有问题啊,代理模式有什么用呢?接下来我们改造下工程,模拟程序发生异常时候,执行以后的结果如何。2.1.3 改造业务类代码在业务层的代码加入一行异常代码,看看结果是否还会转账成功呢?执行结果:当然了,其实提前也能想得到,肯定会执行失败的啦,哈哈哈哈,我们手动加了运算会出现异常的代码嘛!但是转账的动作是不是也失败了呢?我们再来看一下数据库:问题来了: id 为 1 的账号 money 的列值由原来的 900 变成了 800,说明存款确实减少了 100,但是由于在代码执行的过程中,出现了异常,导致原始账号减少 100 的金钱后保存成功, 而 id 为 2 的账号并没有增加 100。这就出现了数据的事务问题,破坏了数据的原子性和一致性。那么如何解决呢? 思路就是将我们的数据操作代码,使用事务控制起来。由于本小节篇幅有限,我们留待下一小节解决。
当我们使用集群的方式部署的服务在不同的机器上时,根据机器的性能以及网络环境,我们可能需要使用负载均衡策略来分配请求到不同的机器,这里我们就开始讲解负载均衡的策略。Round Robin 轮询策略轮询策略,按照集群的服务列表的顺序,依次进行请求的分配,直到列表中所有的服务都分配了一次请求,就完成了一轮的请求分配,然后再从第一个服务开始分配请求。轮询策略是很多负载均衡技术的默认策略,这样的方式保证了的每个服务所承受的请求压力是平均的,我们可以把服务列表按照顺序放到一个数组来循环分配请求。/** * 轮询策略 Demo */public class RoundRobinStrategy { public static void main(String[] args) { // 模拟 Server 地址列表 String[] serverList = {"192.168.0.77","192.168.0.88","192.168.0.99"}; // 模拟 5 次请求 for (int i = 0; i < 5; i++) { // 根据数组长度取模,顺序获取地址索引 int i1 = i % serverList.length; // 根据索引获取服务器地址 System.out.println(serverList[i1]); } }}执行 main 方法,查看控制台输出:192.168.0.77192.168.0.88192.168.0.99192.168.0.77192.168.0.88我们可以观察到控制台输出的服务地址是顺序的。Random 随机策略随机策略,顾名思义就是根据随机算法把请求随机的分配给服务列表中的任意一个服务。随机策略的实现方式:我们可以把服务列表放到一个数组,然后根据数组的长度来获取随机数,取到的随机数就是服务在数组中的索引,根据这个索引,我们就可以拿到服务地址来发送请求了。/** * 随机策略 Demo */public class RandomStrategy { public static void main(String[] args) { // 服务地址数组 String[] serverList = {"192.168.0.77","192.168.0.88","192.168.0.99"}; // 模拟发送 5 次请求 for (int j = 0; j < 5; j++) { // 随机获取数组的索引 int i = new Random().nextInt(serverList.length); // 根据索引获取服务器地址 System.out.println(serverList[i]); } }}执行 main 方法,查看控制台输出:192.168.0.88192.168.0.88192.168.0.99192.168.0.77192.168.0.77我们可以观察到控制台输出的服务地址是随机的,还有可能会出现多次请求连续随机到同一个服务的情况。Consistent Hashing 一致性哈希策略一致性哈希策略的实现方式:我们先把服务列表中的地址进行哈希计算,把计算后的值放到哈希环上,接收到请求后,根据请求的固定属性值来进行哈希计算,然后根据请求的哈希值在哈希环上顺时针寻找服务地址的哈希值,寻找到哪个服务地址的哈希值,就把请求分配给哪个服务。Tips: 哈希环的范围,从 0 开始,到 2 的32 次方减 1 结束,也就是到 Integer 的最大取值范围。在示例的图中,哈希环上有 3 个 Server 的 Hash 值,每个请求的 Hash 值都顺时针去寻找 Server 的 Hash 值,找到哪个就将请求分配给哪个服务。接下来我们用 Java 实现一致性哈希策略,使用 IP 地址进行 Hash 计算:/** * 一致性哈希策略 Demo */public class ConsistentHashingStrategy { public static void main(String[] args) { // 模拟 Server 地址列表 String[] serverList = {"192.168.0.15", "192.168.0.30", "192.168.0.45"}; // 新建 TreeMap 集合 ,以 Key,Value 的方式绑定 Hash 值与地址 SortedMap<Integer, String> serverHashMap = new TreeMap<>(); // 计算 Server 地址的 Hash 值 for (String address : serverList) { int serverHash = Math.abs(address.hashCode()); // 绑定 Hash 值与地址 serverHashMap.put(serverHash, address); } // 模拟 Request 地址 String[] requestList = {"192.168.0.10", "192.168.0.20", "192.168.0.40", "192.168.0.50"}; // 计算 Request 地址的 Hash 值 for (String request : requestList) { int requestHash = Math.abs(request.hashCode()); // 在 serverHashMap 中寻找所有大于 requestHash 的 key SortedMap<Integer, String> tailMap = serverHashMap.tailMap(requestHash); //如果有大于 requestHash 的 key, 第一个 key 就是离 requestHash 最近的 serverHash if (!tailMap.isEmpty()) { Integer key = tailMap.firstKey(); // 根据 key 获取 Server address String address = serverHashMap.get(key); System.out.println("请求 " + request + " 被分配给服务 " + address); } else { // 如果 serverHashMap 中没有比 requestHash 大的 key // 则直接在 serverHashMap 取第一个服务 Integer key = serverHashMap.firstKey(); // 根据 key 获取 Server address String address = serverHashMap.get(key); System.out.println("请求 " + request + " 被分配给服务 " + address); } } }}执行 main 方法,查看控制台输出:请求 192.168.0.10 被分配给服务 192.168.0.15请求 192.168.0.20 被分配给服务 192.168.0.30请求 192.168.0.40 被分配给服务 192.168.0.45请求 192.168.0.50 被分配给服务 192.168.0.15加权轮询策略加权轮询策略就是在轮询策略的基础上,对 Server 地址进行加权处理,除了按照服务地址列表的顺序来分配请求外,还要按照权重大小来决定请求的分配次数。加权的目的是为了让性能和网络较好的服务多承担请求分配的压力。比如 Server_1 的权重是 3,Server_2 的权重是 2,Server_3 的权重是 1,那么在进行请求分配时,Server_1 会被分配 3 次请求,Server_2 会被分配 2 次请求,Server_3 会被分配 1 次请求,就这样完成一轮请求的分配,然后再从 Server_1 开始进行分配。加权随机策略加权随机策略就是在随机策略的基础上,对 Server 地址进行加权处理,Server 地址的加权有多少,那么 Server 地址的数组中的地址就会有几个,然后再从这个数组中进行随机选址。Least Connection 最小连接数策略最小连接数策略,就是根据客户端与服务端会话数量来决定请求的分配情况,它会把请求分配到会话数量小的服务,会话的数量越少,也能说明服务的性能和网络较好。学习完负载均衡的策略,接下来我们使用 Zookeeper 实现负载均衡。
1. 创建 maven 工程:pom 文件的 jar 包坐标如下:<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency></dependencies>2. 实体类 Accountpublic class Account implements Serializable { //数据id private Integer id; //账号编码 private String accountNum; //账号金额 private Float money; //省略get 和set 方法}3. 数据库连接工具类public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * 获取当前线程上的连接 * @return */ public Connection getThreadConnection() { try{ //1.先从ThreadLocal上获取 Connection conn = tl.get(); //2.判断当前线程上是否有连接 if (conn == null) { //3.从数据源中获取一个连接,并且存入ThreadLocal中 conn = dataSource.getConnection(); tl.set(conn); } //4.返回当前线程上的连接 return conn; }catch (Exception e){ throw new RuntimeException(e); } } /** * 把连接和线程解绑 */ public void removeConnection(){ tl.remove(); }}4. 持久层 dao 和 dao 的 实现类://dao的接口public interface IAccountDao { /** * 更新 * @param account */ void updateAccount(Account account); /** * 根据编号查询账户 */ Account findAccountByNum(String accountNum);}//dao的实现类public class AccountDaoImpl implements IAccountDao { //dbutil的查询工具类 private QueryRunner runner; //连接的工具类 private ConnectionUtils connectionUtils; public void setRunner(QueryRunner runner) { this.runner = runner; } public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } //修改账号 public void updateAccount(Account account) { try{ runner.update(connectionUtils.getThreadConnection(),"update account set accountNum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }catch (Exception e) { throw new RuntimeException(e); } } //根据账号查询 public Account findAccountByNum(String accountNum) { try{ List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where accountNum = ? ",new BeanListHandler<Account>(Account.class),accountNum); if(accounts == null || accounts.size() == 0){ return null; } if(accounts.size() > 1){ throw new RuntimeException("结果集不唯一,数据有问题"); } return accounts.get(0); }catch (Exception e) { throw new RuntimeException(e); } }}5. 业务类 Service 和 Service 的实现类//业务接口public interface IAccountService { /** * 转账 * @param sourceAccount 转出账户名称 * @param targetAccount 转入账户名称 * @param money 转账金额 */ void transfer(String sourceAccount, String targetAccount, Integer money);}//业务实现类public class AccountServiceImpl implements IAccountService { //持久层对象 private IAccountDao accountDao; //省略 set 和 get 方法 //转账的方法 public void transfer(String sourceAccount, String targetAccount, Integer money) { //查询原始账户 Account source = accountDao.findAccountByNum(sourceAccount); //查询目标账户 Account target = accountDao.findAccountByNum(targetAccount); //原始账号减钱 source.setMoney(source.getMoney()-money); //目标账号加钱 target.setMoney(target.getMoney()+money); //更新原始账号 accountDao.updateAccount(source); //更新目标账号 accountDao.updateAccount(target); System.out.println("转账完毕"); }}6. 事务管理器类package com.offcn.transaction;/** * @Auther: wyan * @Date: 2020-05-26 21:20 * @Description: */import com.offcn.utils.ConnectionUtils;/** * 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接 */public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 开启事务 */ public void beginTransaction(){ try { System.out.println("开启事务"); connectionUtils.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } /** * 提交事务 */ public void commit(){ try { System.out.println("提交事务"); connectionUtils.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } /** * 回滚事务 */ public void rollback(){ try { System.out.println("回滚事务"); connectionUtils.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } /** * 释放连接 */ public void release(){ try { System.out.println("释放连接"); connectionUtils.getThreadConnection().close();//还回连接池中 connectionUtils.removeConnection(); }catch (Exception e){ e.printStackTrace(); } }}代码解释:此工具类就作为 Spring 使用 AOP 管理事务的通知类,里面的各个方法用于配置 Spring 的通知使用。为了测试效果,在每个通知方法内,我们输出打印了测试语句。7. 配置文件中添加 AOP 的相关配置<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 配置Service --> <bean id="accountService" class="com.offcn.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> </bean> <!--配置Dao对象--> <bean id="accountDao" class="com.offcn.dao.impl.AccountDaoImpl"> <property name="runner" ref="runner"></property> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--连接数据库的必备信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/transmoney"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 配置Connection的工具类 ConnectionUtils --> <bean id="connectionUtils" class="com.offcn.utils.ConnectionUtils"> <!-- 注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器--> <bean id="txManager" class="com.offcn.transaction.TransactionManager"> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!-- aop相关的节点配置 --> <aop:config> <aop:pointcut expression="execution ( * com.offcn.service.*.*(..))" id="pc"/> <aop:aspect ref="txManager"> <aop:before method="beginTransaction" pointcut-ref="pc"/> <aop:after-returning method="commit" pointcut-ref="pc"/> <aop:after method="release" pointcut-ref="pc"/> <aop:after-throwing method="rollback" pointcut-ref="pc"/> </aop:aspect> </aop:config></beans>配置文件说明:connectionUtils: 是获取数据库连接的工具类;dataSource: 采用 c3p0 数据源,大家一定要注意数据库的名称与账号名和密码;queryRunner: dbutils 第三方框架提供用于执行 SQL 语句,操作数据库的一个工具类;accountDao 和 accountService: 是我们自定义的业务层实现类和持久层实现类;aop:config: 此节点是新增加 AOP 配置,AOP 相关信息都在这;aop:pointcut: 此节点是切入点,表示哪些类的哪些方法在执行的时候会应用 Spring 配置的通知进行增强;aop:aspect: 此节点是配置切面类的节点,在 AOP 介绍的小节解释过,它的作用主要就是整合通知和切入点。null 前置、后置、异常、和最终。可以看得出来 before 前置通知执行的方法是开启事务, after-returning 成功执行的方法是提交事务,after 最终执行的方法是释放连接,after-throwing 出现异常执行的方法是回滚。8. 测试类代码@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:applicationContext.xml")public class AccountServiceTest { @Autowired private IAccountService accountService; @Test public void testTransfer(){ accountService.transfer("622200009999","622200001111",100); }}测试结果:执行代码后结果:可以看到,我们通过在 xml 文件中配置 Spring 的 AOP 相关配置,就可以实现对我们业务类中的方法实现了增强,无需自定义对业务类做代理实现。
为了能演示 ORM 中外键的使用,我们在前面的会员 Member 的基础上新增一个关联表:会员等级表(vip_level)。这个会员等级有 VIP、VVIP 以及超级 VIP 的 VVVIP 三个等级,我们在 models.py 中添加如下模型类,并在会员表中添加对应的外键字段,连接到会员等级表中:# hello_app/models.py# ...class VIPLevel(models.Model): name = models.CharField('会员等级名称', max_length=20) price = models.IntegerField('会员价格,元/月', default=10) remark = models.TextField('说明', default="暂无信息") def __str__(self): return "<%s>" % (self.name) class Meta: db_table = 'vip_level' class Member(models.Model): # ... # 添加外键字段 vip_level = models.ForeignKey('VIPLevel', on_delete=models.CASCADE, verbose_name='vip level') # ...# ...首先,我们需要把前面生成的 Member 表删除,同时删除迁移记录文件,操作如下:(django-manual) [root@server first_django_app]# pwd/root/django-manual/first_django_app# 删除迁移记录表(django-manual) [root@server first_django_app]# rm -f hello_app/migrations/0001_initial.py 此外,还需要将数据库中的原 member 表、django_migrations 表删除,即还原到最初状态。接下来,我们使用数据库迁移命令:(django-manual) [root@server first_django_app]# python manage.py makemigrationsMigrations for 'hello_app': hello_app/migrations/0001_initial.py - Create model VIPLevel - Create model Member(django-manual) [root@server first_django_app]# python manage.py migrate hello_appOperations to perform: Apply all migrations: hello_appRunning migrations: Applying hello_app.0001_initial... OK注意: 如果 migrate 后面不带应用会生成许多 Django 内置应用的表,比如权限表、用户表、Session表等。生成的 member 表 上面我们可以看到,我们生成的会员表中相比之前对了一个 vip_level_id 字段,这个字段关联的是 vip_level 表的 id 字段。现在我们首先在 vip_level 中新建三条记录,分别表示 VIP、VVIP 以及 VVVIP:(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from hello_app.models import VIPLevel>>> vip = VIPLevel(name='vip', remark='普通vip', price=10)>>> vip.save()>>> vvip = VIPLevel(name='vvip', remark='高级vip', price=20)>>> vvip.save()>>> vvvip = VIPLevel(name='vvvip', remark='超级vip', price=30)>>> vvvip.save()>>> VIPLevel.objects.all()<QuerySet [<VIPLevel: <vip>>, <VIPLevel: <vvip>>, <VIPLevel: <vvvip>>]>接下来,我们操作 member 表,生成几条记录并关联到 vip_level 表:>>> from hello_app.models import Member>>> m1 = Member(name='会员1', age=29, sex=0, occupation='python', phone_num='18054299999', city='guangzhou')>>> m1.vip_level = vip>>> m1.save()>>> m2 = Member(name='会员2', age=30, sex=1, occupation='java', phone_num='18054299991', city='shanghai')>>> m2.vip_level = vvip>>> m2.save()>>> m3 = Member(name='会员3', age=35, sex=0, occupation='c/c++', phone_num='18054299992', city='beijing')>>> m3.vip_level = vvvip>>> m3.save()查看会员表中生成的数据如下:会员表 可以看到,这里我们并没有直接写 vip_level_id 值,而是将 Member 的 vip_level 属性值直接赋值,然后保存。最后 Django 的 ORM 模型在这里会自动帮我们处理这个关联字段的值,找到关联记录的 id 值,并赋值给该字段。接下来,我们看下外键关联的查询操作:>>> Member.objects.get(age=29).vip_level<VIPLevel: <vip>>>>> type(Member.objects.get(age=29).vip_level)<class 'hello_app.models.VIPLevel'>>>> vip = VIPLevel.objects.get(name='vip')>>> vip.member_set.all()<QuerySet [<Member: <会员1, 18054299999>>]>>>> type(vip.member_set)<class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'>上面的操作示例中我们给出了关联表 vip_level (往往成为主表) 和 member (往往成为子表) 之间的正向和反向查询。在 Django 默认每个主表都有一个外键属性,这个属性值为:从表_set,通过这个属性值我们可以查到对应的从表记录,比如上面的 vip.member_set.all() 语句就是查询所有 vip 会员。当然这个外键属性是可以修改的,我们需要在 member 表中的外键字段那里加上一个属性值:class Member(models.Model): ... vip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.CASCADE, verbose_name='vip level') ...这样我们想再次通过主表查询子表时,就要变成如下方式了:>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> vip = VIPLevel.objects.get(name='vip')>>> vip.member_set.all()Traceback (most recent call last): File "<console>", line 1, in <module>AttributeError: 'VIPLevel' object has no attribute 'member_set'>>> vip.new_name.all()<QuerySet [<Member: <会员1, 18054299999>>]>>>>前面在定义外键时,我们添加了一个 on_delete 属性,这个属性控制着在删除子表外键连接的记录时,对应字表的记录会如何处理,它有如下属性值:CASCADE:级联操作。如果外键对应的那条记录被删除了,那么子表中所有外键为那个记录的数据都会被删除。对于例中,就是如果我们将会员等级 vip 的记录删除,那么所有 vip 会员会被一并删除;# 前面使用的正是CASCADE>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vip')<VIPLevel: <vip>>>>> VIPLevel.objects.get(name='vip').delete()(2, {'hello_app.Member': 1, 'hello_app.VIPLevel': 1})>>> Member.objects.all()<QuerySet [<Member: <会员2, 18054299991>>, <Member: <会员3, 18054299992>>]>PROTECT:受保护。即只要子表中有记录引用了外键的那条记录,那么就不能删除外键的那条记录。如果我们强行删除,Django 就会报 ProtectedError 异常;# 修改外键连接的 on_delete 属性值为 PROTECT>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vvip').delete()Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/base.py", line 918, in delete collector.collect([self], keep_parents=keep_parents) File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/deletion.py", line 224, in collect field.remote_field.on_delete(self, field, sub_objs, self.using) File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/deletion.py", line 22, in PROTECT raise ProtectedError(django.db.models.deletion.ProtectedError: ("Cannot delete some instances of model 'VIPLevel' because they are referenced through a protected foreign key: 'Member.vip_level'", <QuerySet [<Member: <会员2, 18054299991>>]>)SET_NULL:设置为空。如果外键的那条数据被删除了,那么子表中所有外键为该条记录的对应字段值会被设置为 NULL,前提是要指定这个字段可以为空,否则也会报错;# hello_app/models.pyvip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.SET_NULL, verbose_name='vip level', null=True)>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vvip').delete()>>> Member.objects.get(name='会员2').vip_level_id is NoneTrue注意:注意加上null=True是不够的,因为数据库在使用迁移命令时候已经默认是不可为空,这里测试时还需要手动调整下表 vip_level 字段属性,允许为 null。允许 vip_level_id 为 nullSET_DEFAULT:设置默认值。和上面类似,前提是字表的这个字段有默认值;SET():如果外键的那条数据被删除了。那么将会获取SET函数中的值来作为这个外键的值。SET函数可以接收一个可以调用的对象(比如函数或者方法),如果是可以调用的对象,那么会将这个对象调用后的结果作为值返回回去;# hello_app/models.py# 新增一个设置默认值函数def default_value(): # 删除记录时会调用,在这里可以做一些动作 # ... # 返回临时指向一条记录的id,返回不存在的id时会报错;返回数字也会报错,要注意 return '4'# ...class Member(models.Model): # ... vip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.SET(default_value), verbose_name='vip level', null=True) # ...>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objetcs.get(name='会员3').vip_level_id3# 新建一个临时过渡vip记录>>> tmp_vip=VIPLevel(name='等待升级vip', price=30, remark='临时升级过渡')>>> tmp_vip.save()>>> tmp_vip.id4# 删除vvvip记录>>> VIPLevel.objects.all().get(name='vvvip').delete()(1, {'hello_app.VIPLevel': 1} # 可以看到,会员表中曾经指向为vvvip的记录被重新指向了临时过渡vip>>> Member.objects.get(name='会员3').vip_level_id4DO_NOTHING:什么也不做,你删除你的,我保留我的,一切全看数据库级别的约束。在 MySQL 中,这种情况下无法执行删除动作。
//使用序列fun main(args: Array<String>){ (0..100) .asSequence() .map { it + 1 } .filter { it % 2 == 0 } .find { it > 3 }}//使用普通集合fun main(args: Array<String>){ (0..100) .map { it + 1 } .filter { it % 2 == 0 } .find { it > 3 }}通过 decompile 上述例子的源码会发现,普通集合操作会针对每个操作都会生成一个 while 循环,并且每次都会创建新的集合保存中间结果。而使用序列则不会,它们内部会无论进行多少中间操作都是共享同一个迭代器中的数据,想知道共享同一个迭代器中的数据的原理吗?请接着看内部源码实现。6.1 使用集合普通操作反编译源码 public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); byte var1 = 0; Iterable $receiver$iv = (Iterable)(new IntRange(var1, 100)); //创建新的集合存储map后中间结果 Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10))); Iterator var4 = $receiver$iv.iterator(); int it; //对应map操作符生成一个while循环 while(var4.hasNext()) { it = ((IntIterator)var4).nextInt(); Integer var11 = it + 1; //将map变换的元素加入到新集合中 destination$iv$iv.add(var11); } $receiver$iv = (Iterable)((List)destination$iv$iv); //创建新的集合存储filter后中间结果 destination$iv$iv = (Collection)(new ArrayList()); var4 = $receiver$iv.iterator();//拿到map后新集合中的迭代器 //对应filter操作符生成一个while循环 while(var4.hasNext()) { Object element$iv$iv = var4.next(); int it = ((Number)element$iv$iv).intValue(); if (it % 2 == 0) { //将filter过滤的元素加入到新集合中 destination$iv$iv.add(element$iv$iv); } } $receiver$iv = (Iterable)((List)destination$iv$iv); Iterator var13 = $receiver$iv.iterator();//拿到filter后新集合中的迭代器 //对应find操作符生成一个while循环,最后末端操作只需要遍历filter后新集合中的迭代器,取出符合条件数据即可。 while(var13.hasNext()) { Object var14 = var13.next(); it = ((Number)var14).intValue(); if (it > 3) { break; } } }6.2 使用序列 (Sequences) 惰性操作反编译源码1、整个序列操作源码 public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); byte var1 = 0; //利用Sequence扩展函数实现了fitler和map中间操作,最后返回一个Sequence对象。 Sequence var7 = SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE); //取出经过中间操作产生的序列中的迭代器,可以发现进行map、filter中间操作共享了同一个迭代器中数据,每次操作都会产生新的迭代器对象,但是数据是和原来传入迭代器中数据共享,最后进行末端操作的时候只需要遍历这个迭代器中符合条件元素即可。 Iterator var3 = var7.iterator(); //对应find操作符生成一个while循环,最后末端操作只需要遍历filter后新集合中的迭代器,取出符合条件数据即可。 while(var3.hasNext()) { Object var4 = var3.next(); int it = ((Number)var4).intValue(); if (it > 3) { break; } } }2、抽出其中这段关键 code,继续深入:SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE);3、把这段代码转化分解成三个部分://第一部分val collectionSequence = CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100)))//第二部分val mapSequence = SequencesKt.map(collectionSequence, (Function1)null.INSTANCE)//第三部分val filterSequence = SequencesKt.filter(mapSequence, (Function1)null.INSTANCE)4、解释第一部分代码:第一部分反编译的源码很简单,主要是调用 Iterable 中扩展函数将原始数据集转换成 Sequence 对象。public fun <T> Iterable<T>.asSequence(): Sequence<T> { return Sequence { this.iterator() }//传入外部Iterable<T>中的迭代器对象}更深入一层:@kotlin.internal.InlineOnlypublic inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> { override fun iterator(): Iterator<T> = iterator()}通过外部传入的集合中的迭代器方法返回迭代器对象,通过一个对象表达式实例化一个 Sequence,Sequence 是一个接口,内部有个 iterator () 抽象函数返回一个迭代器对象,然后把传入迭代器对象作为 Sequence 内部的迭代器,也就是相当于给迭代器加了 Sequence 序列的外壳,核心迭代器还是由外部传入的迭代器对象,有点偷梁换柱的概念。5、解释第二部分的代码:通过第一部分,成功将普通集合转换成序列 Sequence,然后现在进行 map 操作,实际上调用了 Sequence 扩展函数 map 来实现的val mapSequence = SequencesKt.map(collectionSequence, (Function1)null.INSTANCE)进入 map 扩展函数:public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> { return TransformingSequence(this, transform)}会发现内部会返回一个 TransformingSequence 对象,该对象构造器接收一个 Sequence 类型对象,和一个 transform 的 lambda 表达式,最后返回一个 Sequence 类型对象。我们先暂时解析到这,后面会更加介绍。6、解释第三部分的代码:通过第二部分,进行 map 操作后,然后返回的还是 Sequence 对象,最后再把这个对象进行 filter 操作,filter 也还是 Sequence 的扩展函数,最后返回还是一个 Sequence 对象。val filterSequence = SequencesKt.filter(mapSequence, (Function1)null.INSTANCE)进入 filter 扩展函数:public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> { return FilteringSequence(this, true, predicate)}会发现内部会返回一个 FilteringSequence 对象,该对象构造器接收一个 Sequence 类型对象,和一个 predicate 的 lambda 表达式,最后返回一个 Sequence 类型对象。我们先暂时解析到这,后面会更加介绍。7、Sequences 源码整体结构介绍代码结构图 :图中标注的都是一个个对应各个操作符类,它们都实现 Sequence 接口首先,Sequence 是一个接口,里面只有一个抽象函数,一个返回迭代器对象的函数,可以把它当做一个迭代器对象外壳。public interface Sequence<out T> { /** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */ public operator fun iterator(): Iterator<T>}Sequence 核心类 UML 类图这里只画出了某几个常用操作符的类图注意:通过上面的 UML 类关系图可以得到,共享同一个迭代器中的数据的原理实际上就是利用 Java 设计模式中的状态模式 (面向对象的多态原理) 来实现的,首先通过 Iterable 的 iterator () 返回的迭代器对象去实例化 Sequence,然后外部调用不同的操作符,这些操作符对应着相应的扩展函数,扩展函数内部针对每个不同操作返回实现 Sequence 接口的子类对象,而这些子类又根据不同操作的实现,更改了接口中 iterator () 抽象函数迭代器的实现,返回一个新的迭代器对象,但是迭代的数据则来源于原始迭代器中。8、接着上面 TransformingSequence、FilteringSequence 继续解析.通过以上对 Sequences 整体结构深入分析,那么接着 TransformingSequence、FilteringSequence 继续解析就非常简单了。我们就以 TransformingSequence 为例://实现了Sequence<R>接口,重写了iterator()方法,重写迭代器的实现internal class TransformingSequence<T, R>constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> { override fun iterator(): Iterator<R> = object : Iterator<R> {//根据传入的迭代器对象中的数据,加以操作变换后,构造出一个新的迭代器对象。 val iterator = sequence.iterator()//取得传入Sequence中的迭代器对象 override fun next(): R { return transformer(iterator.next())//将原来的迭代器中数据元素做了transformer转化传入,共享同一个迭代器中的数据。 } override fun hasNext(): Boolean { return iterator.hasNext() } } internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> { return FlatteningSequence<T, R, E>(sequence, transformer, iterator) }}9、源码分析总结序列内部的实现原理是采用状态设计模式,根据不同的操作符的扩展函数,实例化对应的 Sequence 子类对象,每个子类对象重写了 Sequence 接口中的 iterator () 抽象方法,内部实现根据传入的迭代器对象中的数据元素,加以变换、过滤、合并等操作,返回一个新的迭代器对象。这就能解释为什么序列中工作原理是逐个元素执行不同的操作,而不是像普通集合所有元素先执行 A 操作,再所有元素执行 B 操作。这是因为序列内部始终维护着一个迭代器,当一个元素被迭代的时候,就需要依次执行 A,B,C 各个操作后,如果此时没有末端操作,那么值将会存储在 C 的迭代器中,依次执行,等待原始集合中共享的数据被迭代完毕,或者不满足某些条件终止迭代,最后取出 C 迭代器中的数据即可。