[笔记]架构探险-从零开始写JavaWeb框架-2.2. 之使框架具有aop特性-干货,让框架支持事务处理

时间:2021-04-27 13:13:30

http://git.oschina.net/zhuqiang/smart-framework 跟着书上学习的 框架git地址
http://git.oschina.net/zhuqiang/mrweb 依赖上面框架的demo练习


ThreadLocal

ThreadLocal : 本地线程(局部变量),作用其实就是,让每个线程拥有独立的一份变量,比如同一个类中的一个static属性,如果用ThreadLocal来存储,那么无论你new 多少个线程共享这个类,但是这个被static修饰的变量在每个线程里面的值都是唯一的.

用法就不写了.直接模拟实现这个ThreadLocal.他有4个方法.

  1. T get() 返回此线程局部变量的当前线程副本中的值。
  2. protected T initialValue() 返回此线程局部变量的当前线程的“初始值”。
  3. void remove() 移除此线程局部变量当前线程的值。
  4. void set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值。
/** * Created by zhuqiang on 2015/10/29 0029. */
public class MyThreadLocal<T> {
    Map<Thread, T> container = new ConcurrentHashMap<>();

    /** 返回此线程局部变量的当前线程副本中的值。 */
    public T get() {
        Thread ct = Thread.currentThread();
        if (container.containsKey(ct)) {
            return container.get(ct);
        } else { //如果没有值,就返回默认值
            return initialValue();
        }
    }

    /** 返回此线程局部变量的当前线程的“初始值”。 */
    protected T initialValue() {
        return null;
    }

    /** 移除此线程局部变量当前线程的值。 */
    public void remove() {
        Thread ct = Thread.currentThread();
        if (container.containsKey(ct)) {
            container.remove(ct);
        }
    }

    /** 将此线程局部变量的当前线程副本中的值设置为指定值。 */
    public void set(T value) {
        container.put(Thread.currentThread(), value);
    }

}

class Client{
    public static void main(String[] args) {
        MyThreadLocal<String> mt = new MyThreadLocal<>();
        new Thread(new Task(mt)).start();
        new Thread(new Task(mt)).start();
        new Thread(new Task(mt)).start();
    }
}

class Task implements Runnable{
    private MyThreadLocal<String> mt;

    public Task(MyThreadLocal<String> mt) {
        this.mt = mt;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(String.format("当前线程%s,mt中的value=%s", name,mt.get()));
        try {
            // 随机休眠,如果有线程并发问题.或则 如果这样还是共享的一个变量的话.就能看出来端倪
            TimeUnit.MILLISECONDS.sleep((long)(Math.random() * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mt.set(name);

        System.out.println(String.format("改变值后:当前线程%s,mt中的value=%s", name,mt.get()));
    }
}

某一次运行结果

当前线程Thread-2,mt中的value=null
当前线程Thread-1,mt中的value=null
当前线程Thread-0,mt中的value=null
改变值后:当前线程Thread-0,mt中的value=Thread-0
改变值后:当前线程Thread-2,mt中的value=Thread-2
改变值后:当前线程Thread-1,mt中的value=Thread-1

ThreadLocal 底层用的是 ThreadLocalMap来存储,ThreadLocalMap中用Entry数组来实现的.写了那么多.我也看太懂.总之感觉好强大的样子.
他的使用场景在上面自己模拟实现的时候也看到了. 虽然共享的是一个类,但是他们每个线程存储和读取都是独立的. 那么,就可以把它这个特性用到 对事务的控制,对那一对jdbc代码的开关等的封装.


事务管理

什么是事务

  通俗的理解: 一件事情,你要么不做,要么就从头做到尾巴,中途不能退缩. (原子性(Atomicity))
  数据库领域: 执行多条sql有关联的语句,要么都成功要么都失败,经典的例子就是:A打款给B,A扣款成功,那么B要么加款成功,要么就加款失败,如果加款失败,A的扣款操作就必须无效. 否则就保证不了一致性. 还有就是隔离性,如果有多个终端在操作同一条数据,那么就要保证竞态条件的安全性,就如多线程一样的道理.所以就有了事务隔离级别(Transaction Isolation Level):

  1. READ_UNCOMMITTED : 读未提交
  2. READ_COMMITTED : 读已提交
  3. REPEATABLE_READ : 可重复读
  4. SERIALIZABLE : 序列化

百度找的一段描述 :

serializable:

  1. 这种隔离级别对数据的要求最为严格,自然也是性能最差的一种隔离级别。在所有的select语句中都是默认加了一个lock in share mode的锁,
  2. 在这种隔离级别中没有一致读的,所有的select都将返回最近的数据状态。
  3. 由于这种隔离级别的对数据高度一致的严格,所以会产生很多的锁,自然也会导致很多的死锁,对性能的影响不言而喻。

repeatable read:

  1. 所有的select在第一次一致读以后在事务中都会使用一样的数据状态快照。
  2. update,delete都会使用间隙锁来保证数据的安全。防止phantom。
  3. 这是采用最广的事务隔离级别,也是mysql默认的事务隔离级别。

read commited:

  1. 每一个select都会使用各自的数据状态的快照。
  2. 如果当前的数据状态已更新到最新,但是当当个select的时候仍然会产生不一致的数据状态。
  3. 更少的间隙锁意味着更少的死锁。
  4. 唯一key的检查在第二索引和其它外键检查的时候也会产生间隙所。(gap必须被锁定以防止在parent row被删除后仍在child row中插入相关数据)。
  5. 这种隔离级别也是使用的非常普遍的隔离级别尤其是在5.1以后的版本中。
  6. 征对在5.0更早的版本中,可以通过innodb_locks_unsafe_for_binlog移除gap locking。
    (In V5.1, most gap-locking is removed w/ this level, but you MUST use row-based logging/replication。)

read uncommitted:

  1. 这种隔离级别几乎不被使用,在selelct将会看到各种奇怪的数据现象,当然包括其它事务还未提交的数据。
  2. 强烈不推荐,不能保证数据的一致性。

以上四种隔离级别其实就是解决以下对应的几种并发问题的:

  1. 脏读: 事务A读取了事务B未提交的数据,并在这个基础上又做了其他的操作.
  2. 不可重复读: 事务A读取了事务B已提交的更改数据
  3. 幻读(读已提交):事务A读取了事务B已提交的新增数据.
事务隔离级别 脏读 不可重复读 幻读
READ_UNCOMMITTED
READ_COMMITTED ×
REPEATABLE_READ × ×
SERIALIZABLE × × ×

Mysql的默认级别是:REPEATABLE_READ,原子性是基础,隔离性是手段,持久性是目的.一致性是并发的最终安全目的.


Spring事务的传播行为

其实我有不明白的地方,比如:这些事务,好像和数据库没的事务没有什么关联吧,我觉得只是增强了代码中处理复杂方法的一些手段.

spring的事务传播行为:

  1. PROPAGATION_REQUIRED–支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
  2. PROPAGATION_SUPPORTS–支持当前事务,如果当前没有事务,就以非事务方式执行。
  3. PROPAGATION_MANDATORY–支持当前事务,如果当前没有事务,就抛出异常。
  4. PROPAGATION_REQUIRES_NEW–新建事务,如果当前存在事务,把当前事务挂起。
  5. PROPAGATION_NOT_SUPPORTED–以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  6. PROPAGATION_NEVER–以非事务方式执行,如果当前存在事务,则抛出异常。
  7. PROPAGATION_NESTED–如果没有就新建事务,如果有,就在当前事务中嵌套其他事务,所嵌套的子事务与主事务之间是有关联的(当主事务提交或回滚,子事务也会提交或回滚)

还支持一些附加功能:

  1. 事务超时(Transaction Timeout): 为了解决事务时间太长,小号太多资源的往年提,所以故意设置一个最大时长,如果超时了,就回滚事务.
  2. 只读事物(Readonly Transaction) : 为了忽略哪些不需要事务的方法,比如只读取数据.就可以提高一点性能.

至于使用,和配置什么的就不演示了,看了上面的, 我觉得如果要来模拟实现上面的这些特征,简单粗暴的模拟的话,都太困难,只能做到其中的一些, 用aop来做,得最好多逻辑判断呀.也只能想到这么一点了.


事务管理总结思维图

[笔记]架构探险-从零开始写JavaWeb框架-2.2. 之使框架具有aop特性-干货,让框架支持事务处理

实现事务控制特性

  诶。本来很早就能写完的。 后来电脑老蓝屏,10分钟就蓝屏两次,把代码搞丢了。从win10装回win7,环境什么的浪费了好几天。现在都不太记得思路了。 非常不好意思。这章的运行流程什么的。我自己都不太记得起前面章节那么清楚了。 只能写成这样了。只能以后自己做框架的时候 才能真正的理解明白 他这个运行流程了。

实现思路:

  1. 首先编写Transaction注解类,标识哪些方法是需要事务的。
  2. 编写事务的切面代理类。我们需要在这个里面处理 哪些方法需要进行事务等操作。
  3. 让框架支持 这个事务代理类。
    1. 前一篇讲了 怎么支持AspectProxy普通切面类,这里也一样,让框架支持 事务切面代理类。
    2. 需要对 目标类 和 增强它的切面实例进行关联。
    3. 创建代理类,在创建代理类的时候,还是在intercept方法中返回 我们自定义的代理链条。在执行该方法的时候,就会由 代理连框架去 处理调用 切面类。从而完成切面增强操作。
    4. 最后使用创建好的代理类,替换掉bean容器中的普通代理对象
/** * Created by zhuqiang on 2015/11/2 0002. * 事务注解 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Transaction {

}

/** * @author zhuqiang * @version V1.0 * @date 2015/11/5 20:59 * 事务界面代理类 */
public class TransactionProxy implements Proxy {
    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(TransactionProxy.class);
    private static final ThreadLocal<Boolean> FLAG_HOLDER = new ThreadLocal<Boolean>(){  //用来存标志是否已经开启了事务
        @Override
        protected Boolean initialValue() {
            return false;
        }
    };
    @Override
    public Object doProxy(ProxyChain proxyChain) throws Throwable {
        Method method = proxyChain.getTargetMethod();
        Boolean flag = FLAG_HOLDER.get();
        Object result ;
        if(!flag && method.isAnnotationPresent(Transaction.class)){ //该方法还没有开启过事务,并且该方法有事务注解
            try {
                FLAG_HOLDER.set(true);
                DataBaseHelper.beginTransaction();  // 我们把jdbc的操作都封装在了 这个类里面。开启事务
                LOGGER.debug("begin transaction");
                result = proxyChain.doProxyChain();
                DataBaseHelper.commitTracsaction(); //提交事务
                LOGGER.debug("commit transaction");
            }catch (Exception e){
                DataBaseHelper.roolbackTransaction();  //r如果在执行目标(代理链条)方法的时候出现了错误,就回滚事务
                LOGGER.debug("roolback transaction");
                throw e;
            }finally {
                FLAG_HOLDER.remove();
            }
        }else{ // 如果没有事务注解,就直接执行代理链
            result = proxyChain.doProxyChain();
        }
        return result;
    }
}

// 添加 对该代理的支持,把之前的aopHelper稍微的修改了下
public final class AopHelper {
    /** * 获取需要被代理的 所有 class 对象 * @return key= 切面 ,value = 该切面所增强的所有 class 对象 * @throws Exception */
    private static Map<Class<?>,Set<Class<?>>> createProxyMap() throws Exception{
        Map<Class<?>,Set<Class<?>>> proxyMap = new HashMap<>();
        addAspectProxy(proxyMap);
        addTransactionProxy(proxyMap);
        return proxyMap;
    }

    /** * 关联普通界面代理类 * @param proxyMap */
    private static void addAspectProxy(Map<Class<?>,Set<Class<?>>> proxyMap) throws Exception {
        // 获取 所有的切面代理类 的 子类
        Set<Class<?>> aspectProxySet = ClassHelper.getClassSetBySuper(AspectProxy.class);
        for (Class<?> cls : aspectProxySet) {
            if(cls.isAnnotationPresent(Aspect.class)){// 判断该class 是否有 aspect注解(是否是一个切面标注)
                Aspect aspect = cls.getAnnotation(Aspect.class); // 获取注解类
                Set<Class<?>> targetClassSet = createTargetClassSet(aspect); // 由于 aspect 的值是接收一个 注解类,所以这里是获取到 使用该注解类的所有class
                proxyMap.put(cls,targetClassSet);
            }
        }
    }

    /** * 关联事务代理 * @param proxyMap * @throws Exception */
    private static void addTransactionProxy(Map<Class<?>,Set<Class<?>>> proxyMap) throws Exception {
        // 获取 所有的service类,这里是简单的处理,默认就把services注解标识的类。都拿过来换成事务代理对象,然后在代理执行的时候,会根据
        //是否有Transaction注解。而去对事务进行操作。
        Set<Class<?>> servicesProxySet = ClassHelper.getClassSetByAnnotation(Services.class);
        proxyMap.put(TransactionProxy.class,servicesProxySet); //将 切面 与 目标类进行关联
    }
}
/** * Created by zhuqiang on 2015/10/17 0017. * 数据库助手类 */
public class DataBaseHelper {
    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(DataBaseHelper.class);
    //用于存放当前线程的 sql 链接对象
    private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<Connection>();
    ************  其他代码 请看 git中的。 或则以前的章节    

    /** 开启事务 */
    public static void beginTransaction(){
        Connection conn = getConnection();
        if(conn != null){
            try {
                conn.setAutoCommit(false); //关闭自动提交,每次操作数据库都需要手动的提交(也就是开启了事务)
            } catch (SQLException e) {
                LOGGER.error("begin transaction failure",e);
                throw  new RuntimeException(e);
            }finally {
                CONNECTION_HOLDER.set(conn);
            }
        }
    }

    /** 提交事务 **/
    public static void commitTracsaction(){
        Connection conn = getConnection();
        if(conn != null){
            try {
                conn.commit(); //提交事务
                conn.close();
            } catch (SQLException e) {
                LOGGER.error("commit tracsaction failure",e);
                throw new RuntimeException(e);
            }finally {
                CONNECTION_HOLDER.remove(); //移除链接
            }
        }
    }

    /** 回滚事务 */
    public static void roolbackTransaction(){
        Connection conn = getConnection();
        if(conn != null){
            try {
                conn.rollback();
                conn.close();
            } catch (SQLException e) {
                LOGGER.error("roolback transaction failure",e);
                throw new RuntimeException(e);
            }finally {
                CONNECTION_HOLDER.remove();
            }
        }
    }
}

最后整理下思路:
让框架支持aop的流程:
比如,为action层所有方法加上执行方法耗时的功能。

  1. 创建增强这个功能的切面类
  2. 创建普通切面注解类,里面接受一个注解类型(比如放入Controller注解,那么就表示,被普通切面注解类标注的切面类,增强的对象就是 所有的被Controller注解标注的对象)
  3. 在框架初始化的时候需要 框架创建出这些被增强的Controller类的代理对象,并放入bean容器中。(然后再执行 注入处理)
  4. 在运行的时候,会通过代理类去调用目标方法,而我们的代理类是用代理链机制去处理增强目标对象的处理。

* 然而,框架是怎么实现对aop的支持的呢?:*

  1. 首先,我们要把框架所管理的类class拿到(ClassHelper)
  2. 然后实例化这些class。接下来应该就是注入了,但是由于有了aop,下面一步需要把代理对象替换掉bean容器中的普通实例对象(BeanHelper)
  3. AopHelper出场了,它的最终作用,就是对 被增强的 目标对象建立代理对象。然后替换掉bean容器中的普通对象
    1. 获取 切面 和 它对应的需要增强的所有的目标 class类。
    2. 通过第一步,然后反转过来,获取,目标class类 与对应的 切面切面实例进行关联(因为一个目标类可以被多个切面进行增强)【这一部很重要,因为要拿到所有增强这个目标类的切面列表】
    3. 通过第2步,有了目标class类,也有了切面实例列表,然后 使用cglib创建出代理对象,在代理方法中,把 切面列表交给我们的 代理链类进行处理。
  4. 对需要依赖的属性,进行注入操作。 (IocHelper)

– 我感觉这个流程其实蛮复杂的。主要那个代理链的处理,如果最开始没有想到的话。那么这一套处理流程的实现 可能都写不出来。