首页 文章 精选 留言 我的

精选列表

搜索[图像理解],共10000篇文章
优秀的个人博客,低调大师

深入理解分布式事务

1、什么是分布式事务 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 2、分布式事务的产生的原因 2.1、数据库分库分表 当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。 2.2、应用SOA化 所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。 以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了! 3、事务的ACID特性 3.1、原子性(A) 所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。 3.2、一致性(C) 事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。 3.3、隔离性(I) 所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。 3.4、持久性(D) 所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。 4、分布式事务的应用场景 4.1、支付 最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。 4.2、在线下单 买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。 5、常见的分布式事务解决方案 5.1、基于XA协议的两阶段提交 XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下: 总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。 5.2、消息事务+最终一致性 所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下: 1、A系统向消息中间件发送一条预备消息 2、消息中间件保存预备消息并返回成功 3、A执行本地事务 4、A发送提交消息给消息中间件 通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析: 步骤一出错,则整个事务失败,不会执行A的本地操作 步骤二出错,则整个事务失败,不会执行A的本地操作 步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息 步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务 基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下: 虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。 5.3、TCC编程模式 所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。 6、总结 分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力! 原文链接:http://www.codeceo.com/article/distributed-transaction.html 作者:吴极心

优秀的个人博客,低调大师

深入理解 Java 动态代理机制

Java 有两种代理方式,一种是静态代理,另一种是动态代理。对于静态代理,其实就是通过依赖注入,对对象进行封装,不让外部知道实现的细节。很多 API 就是通过这种形式来封装的。 代理模式结构图(图片来自《大话设计模式》) 下面看下两者在概念上的解释: 静态代理 静态代理类:由程序员创建或者由第三方工具生成,再进行编译;在程序运行之前,代理类的.class文件已经存在了。 静态代理类通常只代理一个类。 静态代理事先知道要代理的是什么。 动态代理 动态代理类:在程序运行时,通过反射机制动态生成。 动态代理类通常代理接口下的所有类。 动态代理事先不知道要代理的是什么,只有在运行的时候才能确定。 动态代理的调用处理程序必须事先InvocationHandler接口,及使用Proxy类中的newProxyInstance方法动态的创建代理类。 Java动态代理只能代理接口,要代理类需要使用第三方的CLIGB等类库。 动态代理的好处 Java动态代理的优势是实现无侵入式的代码扩展,也就是方法的增强;让你可以在不用修改源码的情况下,增强一些方法;在方法的前后你可以做你任何想做的事情(甚至不去执行这个方法就可以)。此外,也可以减少代码量,如果采用静态代理,类的方法比较多的时候,得手写大量代码。 动态代理实例 静态代理的实例这里就不说了,比较简单。在 java 的 java.lang.reflect 包下提供了一个 Proxy 类和一个 InvocationHandler 接口,通过这个类和这个接口可以生成 JDK 动态代理类和动态代理对象。下面讲讲动态代理的实现。 先定义一个接口: publicinterfacePerson{ voidsetName(Stringname); } 再定义一个学生 Student 类来实现 Person 接口,每一个学生都有一个自己的名字: publicclassStudentimplementsPerson{ privateStringmName; publicStudent(Stringname){ mName=name; } publicvoidsetName(Stringname){ mName=name; } } Student 类中,定义了一个私有变量 mName,用来表示 Student 的名字。接下去定义一个代理 handler,就是用来帮我们处理代理的 : publicclassPersonHandler<T>implementsInvocationHandler{ //代理的目标对象 privateTmTarget; publicPersonHandler(Ttarget){ mTarget=target; } @Override publicObjectinvoke(Objecto,Methodmethod,Object[]objects)throwsThrowable{ //调用开始前的操作 ProxyUtil.start(); //调用方法,通过反射的形式来调用mTarget的method Objectresult=method.invoke(mTarget,objects); //打印改名前的名字 ProxyUtil.log(objects[0].toString()); //调用结束后的操作 ProxyUtil.finish(); returnresult; } } 可以发现,我们在调用代码前后都做了一些操作,甚至可以直接拦截该方法,不让其运行。其中定义了一个 ProxyUtil 类,方便我们做一些操作: publicclassProxyUtil{ privatestaticfinalStringTAG="ProxyUtil"; publicstaticvoidstart(){ Log.d(TAG,"start:"+System.currentTimeMillis()); } publicstaticvoidfinish(){ Log.d(TAG,"finish:"+System.currentTimeMillis()); } publicstaticvoidlog(Stringname){ Log.d(TAG,"log:"+name); } } 接下去开始编写代理的实现: publicclassMainActivityextendsAppCompatActivity{ @Override protectedvoidonCreate(BundlesavedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //创建一个实例对象,这个对象是被代理的对象 Personzhangsan=newStudent("张三"); //创建一个与代理对象相关联的InvocationHandler PersonHandlerstuHandler=newPersonHandler<>(zhangsan); //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法 PersonstuProxy=(Person)Proxy.newProxyInstance(Person.class.getClassLoader(),newClass<?>[]{Person.class},stuHandler); //代理执行setName的方法 stuProxy.setName("王五"); } 看下打印输出: 可以发现代理成功了。并且我们在调用方式的之前之后,都做了一些操作。Spring 的 AOP 其就是通过动态代理的机制实现的。 其中,我们将 stuProxy 的类名打印出来: Log.d(TAG,"onCreate:"+stuProxy.getClass().getCanonicalName()); 发现其名字竟然是 $Proxy0。具体原因下面会解释。 源码分析 上面我们利用 Proxy 类的 newProxyInstance 方法创建了一个动态代理对象,查看该方法的源码: /** *Returnsaninstanceofaproxyclassforthespecifiedinterfaces *thatdispatchesmethodinvocationstothespecifiedinvocation *handler. * *<p>{@codeProxy.newProxyInstance}throws *{@codeIllegalArgumentException}forthesamereasonsthat *{@codeProxy.getProxyClass}does. * *@paramloadertheclassloadertodefinetheproxyclass *@paraminterfacesthelistofinterfacesfortheproxyclass *toimplement *@paramhtheinvocationhandlertodispatchmethodinvocationsto *@returnaproxyinstancewiththespecifiedinvocationhandlerofa *proxyclassthatisdefinedbythespecifiedclassloader *andthatimplementsthespecifiedinterfaces *@throwsIllegalArgumentExceptionifanyoftherestrictionsonthe *parametersthatmaybepassedto{@codegetProxyClass} *areviolated *@throwsSecurityExceptionifasecuritymanager,<em>s</em>,ispresent *andanyofthefollowingconditionsismet: *<ul> *<li>thegiven{@codeloader}is{@codenull}and *thecaller'sclassloaderisnot{@codenull}andthe *invocationof{@linkSecurityManager#checkPermission *s.checkPermission}with *{@codeRuntimePermission("getClassLoader")}permission *deniesaccess;</li> *<li>foreachproxyinterface,{@codeintf}, *thecaller'sclassloaderisnotthesameasoran *ancestoroftheclassloaderfor{@codeintf}and *invocationof{@linkSecurityManager#checkPackageAccess *s.checkPackageAccess()}deniesaccessto{@codeintf};</li> *<li>anyofthegivenproxyinterfacesisnon-publicandthe *callerclassisnotinthesame{@linkplainPackageruntimepackage} *asthenon-publicinterfaceandtheinvocationof *{@linkSecurityManager#checkPermissions.checkPermission}with *{@codeReflectPermission("newProxyInPackage.{packagename}")} *permissiondeniesaccess.</li> *</ul> *@throwsNullPointerExceptionifthe{@codeinterfaces}array *argumentoranyofitselementsare{@codenull},or *iftheinvocationhandler,{@codeh},is *{@codenull} */ @CallerSensitive publicstaticObjectnewProxyInstance(ClassLoaderloader, Class<?>[]interfaces, InvocationHandlerh) throwsIllegalArgumentException { //判空,判断h对象是否为空,为空就抛出NullPointerException Objects.requireNonNull(h); finalClass<?>[]intfs=interfaces.clone(); finalSecurityManagersm=System.getSecurityManager(); if(sm!=null){ //进行包访问权限、类加载器等权限检查 checkProxyAccess(Reflection.getCallerClass(),loader,intfs); } /* *Lookuporgeneratethedesignatedproxyclass. */ Class<?>cl=getProxyClass0(loader,intfs); /* *Invokeitsconstructorwiththedesignatedinvocationhandler. */ try{ if(sm!=null){ checkNewProxyPermission(Reflection.getCallerClass(),cl); } finalConstructor<?>cons=cl.getConstructor(constructorParams); finalInvocationHandlerih=h; if(!Modifier.isPublic(cl.getModifiers())){ AccessController.doPrivileged(newPrivilegedAction<Void>(){ publicVoidrun(){ cons.setAccessible(true); returnnull; } }); } returncons.newInstance(newObject[]{h}); }catch(IllegalAccessException|InstantiationExceptione){ thrownewInternalError(e.toString(),e); }catch(InvocationTargetExceptione){ Throwablet=e.getCause(); if(tinstanceofRuntimeException){ throw(RuntimeException)t; }else{ thrownewInternalError(t.toString(),t); } }catch(NoSuchMethodExceptione){ thrownewInternalError(e.toString(),e); } } 在生成代理类的过程中,会进行一些列检查,比如访问权限之类的。接下去我们来看 getProxyClass0 方法的源码: /** *Generateaproxyclass.MustcallthecheckProxyAccessmethod *toperformpermissionchecksbeforecallingthis. */ privatestaticClass<?>getProxyClass0(ClassLoaderloader, Class<?>...interfaces){ //数量超过65535就抛出异常,665535这个就不用说了吧 if(interfaces.length>65535){ thrownewIllegalArgumentException("interfacelimitexceeded"); } //Iftheproxyclassdefinedbythegivenloaderimplementing //thegiveninterfacesexists,thiswillsimplyreturnthecachedcopy; //otherwise,itwillcreatetheproxyclassviatheProxyClassFactory returnproxyClassCache.get(loader,interfaces); } 最后发现会对生成的代理类进行缓存,有了,就不直接返回,没有的,还得生成代理类,我们继续往下走: proxyClassCache=newWeakCache<>(newKeyFactory(),newProxyClassFactory()); 关键点在于 ProxyClassFactory 这个类,从名字也可以猜出来这个类的作用。看看代码: /** *Afactoryfunctionthatgenerates,definesandreturnstheproxyclassgiven *theClassLoaderandarrayofinterfaces. */ privatestaticfinalclassProxyClassFactory implementsBiFunction<ClassLoader,Class<?>[],Class<?>> { //prefixforallproxyclassnames定义前缀 privatestaticfinalStringproxyClassNamePrefix="$Proxy"; //nextnumbertouseforgenerationofuniqueproxyclassnames原子操作,适用于多线程 privatestaticfinalAtomicLongnextUniqueNumber=newAtomicLong(); @Override publicClass<?>apply(ClassLoaderloader,Class<?>[]interfaces){ Map<Class<?>,Boolean>interfaceSet=newIdentityHashMap<>(interfaces.length); for(Class<?>intf:interfaces){ /* *Verifythattheclassloaderresolvesthenameofthis *interfacetothesameClassobject. */ Class<?>interfaceClass=null; try{ //通过反射获取到接口类 interfaceClass=Class.forName(intf.getName(),false,loader); }catch(ClassNotFoundExceptione){ } //所得到的接口类与传进来的不相等,说明不是同一个类 if(interfaceClass!=intf){ thrownewIllegalArgumentException( intf+"isnotvisiblefromclassloader"); } /* *VerifythattheClassobjectactuallyrepresentsan *interface. */ if(!interfaceClass.isInterface()){ thrownewIllegalArgumentException( interfaceClass.getName()+"isnotaninterface"); } /* *Verifythatthisinterfaceisnotaduplicate. */ if(interfaceSet.put(interfaceClass,Boolean.TRUE)!=null){ thrownewIllegalArgumentException( "repeatedinterface:"+interfaceClass.getName()); } } StringproxyPkg=null;//packagetodefineproxyclassin intaccessFlags=Modifier.PUBLIC|Modifier.FINAL; /* *Recordthepackageofanon-publicproxyinterfacesothatthe *proxyclasswillbedefinedinthesamepackage.Verifythat *allnon-publicproxyinterfacesareinthesamepackage. */ for(Class<?>intf:interfaces){ intflags=intf.getModifiers(); if(!Modifier.isPublic(flags)){ accessFlags=Modifier.FINAL; Stringname=intf.getName(); intn=name.lastIndexOf('.'); Stringpkg=((n==-1)?"":name.substring(0,n+1)); if(proxyPkg==null){ proxyPkg=pkg; }elseif(!pkg.equals(proxyPkg)){ thrownewIllegalArgumentException( "non-publicinterfacesfromdifferentpackages"); } } } if(proxyPkg==null){ //ifnonon-publicproxyinterfaces,usecom.sun.proxypackage proxyPkg=ReflectUtil.PROXY_PACKAGE+"."; } /* *Chooseanamefortheproxyclasstogenerate. */ longnum=nextUniqueNumber.getAndIncrement(); //生产代理类的名字 StringproxyName=proxyPkg+proxyClassNamePrefix+num; /* *Generatethespecifiedproxyclass. */ byte[]proxyClassFile=ProxyGenerator.generateProxyClass( proxyName,interfaces,accessFlags); try{ returndefineClass0(loader,proxyName, proxyClassFile,0,proxyClassFile.length); }catch(ClassFormatErrore){ /* *AClassFormatErrorheremeansthat(barringbugsinthe *proxyclassgenerationcode)therewassomeother *invalidaspectoftheargumentssuppliedtotheproxy *classcreation(suchasvirtualmachinelimitations *exceeded). */ thrownewIllegalArgumentException(e.toString()); } } } 这里会通过反射获取接口的各种修饰符,包名等,然后根据规则命名代理类,最后调用 ProxyGenerator.generateProxyClass 生成了代理类。 ProxyGenerator.generateProxyClass 具体实现在 eclipse 上打开后,说是找不到源码: 不过,从其他地方找到了部分代码: publicstaticbyte[]generateProxyClass(finalStringname, Class[]interfaces) { ProxyGeneratorgen=newProxyGenerator(name,interfaces); //这里动态生成代理类的字节码,由于比较复杂就不进去看了 finalbyte[]classFile=gen.generateClassFile(); //如果saveGeneratedFiles的值为true,则会把所生成的代理类的字节码保存到硬盘上 if(saveGeneratedFiles){ java.security.AccessController.doPrivileged( newjava.security.PrivilegedAction<Void>(){ publicVoidrun(){ try{ FileOutputStreamfile= newFileOutputStream(dotToSlash(name)+".class"); file.write(classFile); file.close(); returnnull; }catch(IOExceptione){ thrownewInternalError( "I/Oexceptionsavinggeneratedfile:"+e); } } }); } //返回代理类的字节码 returnclassFile; } 我们可以自己试试 ProxyGenerator.generateProxyClass 的功能。 publicclassProxyGeneratorUtils{ /** *把代理类的字节码写到硬盘上 *@parampath保存路径 */ publicstaticvoidwriteProxyClassToHardDisk(Stringpath){ //获取代理类的字节码 byte[]classFile=ProxyGenerator.generateProxyClass("$Proxy11",Student.class.getInterfaces()); FileOutputStreamout=null; try{ out=newFileOutputStream(path); out.write(classFile); out.flush(); }catch(Exceptione){ e.printStackTrace(); }finally{ try{ out.close(); }catch(IOExceptione){ e.printStackTrace(); } } } } main 方法里面进行调用 : publicclassMain{ publicstaticvoidmain(String[]args){ ProxyGeneratorUtils.writeProxyClassToHardDisk("$Proxy0.class"); } } 可以发现,在根目录下生成了一个 $Proxy0.class 文件,文件内容反编译后如下: importjava.lang.reflect.InvocationHandler; importjava.lang.reflect.Method; importjava.lang.reflect.Proxy; importjava.lang.reflect.UndeclaredThrowableException; importproxy.Person; publicfinalclass$Proxy0extendsProxyimplementsPerson { privatestaticMethodm1; privatestaticMethodm2; privatestaticMethodm3; privatestaticMethodm0; /** *注意这里是生成代理类的构造方法,方法参数为InvocationHandler类型,看到这,是不是就有点明白 *为何代理对象调用方法都是执行InvocationHandler中的invoke方法,而InvocationHandler又持有一个 *被代理对象的实例,不禁会想难道是....?没错,就是你想的那样。 * *super(paramInvocationHandler),是调用父类Proxy的构造方法。 *父类持有:protectedInvocationHandlerh; *Proxy构造方法: *protectedProxy(InvocationHandlerh){ *Objects.requireNonNull(h); *this.h=h; *} * */ public$Proxy0(InvocationHandlerparamInvocationHandler) throws { super(paramInvocationHandler); } //这个静态块本来是在最后的,我把它拿到前面来,方便描述 static { try { //看看这儿静态块儿里面有什么,是不是找到了giveMoney方法。请记住giveMoney通过反射得到的名字m3,其他的先不管 m1=Class.forName("java.lang.Object").getMethod("equals",newClass[]{Class.forName("java.lang.Object")}); m2=Class.forName("java.lang.Object").getMethod("toString",newClass[0]); m3=Class.forName("proxy.Person").getMethod("giveMoney",newClass[0]); m0=Class.forName("java.lang.Object").getMethod("hashCode",newClass[0]); return; } catch(NoSuchMethodExceptionlocalNoSuchMethodException) { thrownewNoSuchMethodError(localNoSuchMethodException.getMessage()); } catch(ClassNotFoundExceptionlocalClassNotFoundException) { thrownewNoClassDefFoundError(localClassNotFoundException.getMessage()); } } /** * *这里调用代理对象的giveMoney方法,直接就调用了InvocationHandler中的invoke方法,并把m3传了进去。 *this.h.invoke(this,m3,null);这里简单,明了。 *来,再想想,代理对象持有一个InvocationHandler对象,InvocationHandler对象持有一个被代理的对象, *再联系到InvacationHandler中的invoke方法。嗯,就是这样。 */ publicfinalvoidgiveMoney() throws { try { this.h.invoke(this,m3,null); return; } catch(Error|RuntimeExceptionlocalError) { throwlocalError; } catch(ThrowablelocalThrowable) { thrownewUndeclaredThrowableException(localThrowable); } } //注意,这里为了节省篇幅,省去了toString,hashCode、equals方法的内容。原理和giveMoney方法一毛一样。 } jdk 为我们的生成了一个叫 $Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。 我们可以对 InvocationHandler 看做一个中介类,中介类持有一个被代理对象,在 invoke 方法中调用了被代理对象的相应方法,而生成的代理类中持有中介类,因此,当我们在调用代理类的时候,就是再调用中介类的 invoke 方法,通过反射转为对被代理对象的调用。 代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的 invoke 方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。 生成的代理类:$Proxy0 extends Proxy implements Person,我们看到代理类继承了 Proxy 类,所以也就决定了 java 动态代理只能对接口进行代理,Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理。

优秀的个人博客,低调大师

从小白的视角理解广告系统

开篇 因为一些原因有幸接触了一些广告相关的工程技术,结合周末随便翻阅的计算广告学一书,写一篇博文沉淀一下。 文章主要从像我这样的小白用户视角,聚焦于广告相关的工程方向,当然文章当中绝对不会出现违规的内部信息,所以各位大佬们也可以放心了,文章主要会围绕计算广告学中提到的系统架构图进行说明。 其实广告真正核心的内容在于算法部分,工程只是脚手架,一些广告系统用到的软件基本上都是开源的系统,可以在文章末尾的公开课中看到,里面有一章讲解如何用开源软件搭建一个广告系统。 广告系统架构图 广告系统架构 说明: 从上图可以看出,从在线和离线的角度来看广告可以区分为在线系统和离线系统。 离线系统:主要聚焦于算法模型的训练以及离线广告费用结算相关。 在线系统:在线系统主要解决广告投放和实时点击计费相关功能。 在线系统:包括广告投放引擎、广告索引、广告商家中心、计费中心。 投放引擎:主要负责在接受广告投放请求返回对应的广告商品。 广告索引:广告索引保存商家投放的广告商品信息。 广告商家中心:负责提供给商家购买广告的操作平台。 计费中心:实时对商家的广告消耗进行计费并实时实现广告商品的上下架功能。 结算中心:负责对广告的消耗进行离线的结算,对商家进行扣款等。 广告系统核心模块 广告商家中心 广告的商家中心作为商家购买广告的入口,给商家提供购买广告的入口,在这里商家可以为建立自己的推广计划,为自己热推的商品购买广告词,设置一些通用的信息类似点击费用、日推广上线等信息。 购买广告成功后会将商品的广告信息实时的进入到广告索引当中,当用户搜索商品的时候投放引擎就能从广告索引中检索到该广告商品。 广告索引 广告索引顾名思义就是用于保存广告商品的索引信息,便于投放引擎检索广告信息。在广告索引的选型上一般的公司都会选用比较成熟的elasticsearch作为广告索引的技术方案。 作为广告的索引引擎,elasticsearch很好的解决了分词和倒排等功能,当然elasticsearch开放的plugin接口能够提供可供扩展的打分排序的功能。毕竟elasticsearch都已经是一个上市公司了,功能还是比较强大的。 当然在建索引的时机选择上应该都会有一些通用的全量和增量实现方式,这些功能感觉各家之间应该也是大同小异的。 投放引擎 投放引擎作为整个广告系统的门面,主要是负责接受广告请求并返回对应的广告商品,主要是作为一个入口串联起后面的广告引擎等系统。 投放引擎在返回的广告商品信息中,不出意外的会携带一些广告的标识信息,包括商品本身信息和广告的一些计费信息,毕竟这些信息在用户点击广告后会通过日志流的形式传给后端系统用于在线计费和离线结算等。 计费中心 计费中心在这里主要是指实时的在线计费功能,也就是说用户在电商平台实时点击广告后回传一条实时日志给后台系统,后台的计费中心会消费该日志并对这个广告商品进行实时计费。 实时计费中心,很大一部分功能解决广告主也就是电商平台的商家对广告消耗的上限设定,类似于某商家规定一天消耗不超过500。实时结算中心一方面通过实时监控广告消耗解决了广告主投放广告产生消耗上限的需求,同时也让电商平台的广告收益最大化,下线达到上限的广告,让其他广告的曝光增加,增加收入。 当然计费中心内部肯定会包含一些通用的反作弊功能,避免恶意广告点击,这些都是比较通用的功能。 结算中心 结算中心一般对广告前一天的广告消耗进行扣费结算,这其中是一个非常复杂的离线处理系统,需要考虑各种情况。一般结算中心在实时性方面都是T+1,也就是在第二天对前一天广告的消耗进行离线计算并对广告主进行扣费。 广告系统中的开源软件 1、elasticsearch:广告索引 2、kafka:广告点击实时日志 3、redis:缓存反作弊 4、flume:日志采集系统 参考文章 计算广告学-网易云课堂计算广告学

优秀的个人博客,低调大师

我理解的foreach, for in, for of 之间的异同

我所lforEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。 注意:forEach() 对于空数组是不会执行回调函数的。 示例代码: var arr = [4, 9, 16, 25]; arr.forEach(function(value, index) { console.log(index+': '+value+'\n'); }); 结果如下: for/in 语句用于循环对象属性。 循环中的代码每执行一次,就会对数组的元素或者对象的属性进行一次操作。 示例代码: var person = { name: 'jack', age: 18, gender: 'male' }; for(key in person) { console.log(key+': '+person[key]+'\n'); } 结果如下: 需要注意的是,如果使用for in语句遍历数组,可能会出现以下问题: 1. 索引为字符串类型,不能直接进行几何运算。 2. 遍历顺序有可能不是按照实际数组的内部顺序。 3. 使用for in会遍历数组所有的可枚举属性,包括原型。 如下所示: var arr = [12, 43, 19]; Array.prototype.index = 110;//这里的原型属性也会被打印出来 for(var index in arr) { console.log(index+': '+arr[index]+'\n'); } 运行结果: for of语句和for in 语句相比使用更方便,它是ES6新提出的遍历数组的方式, 具体使用: var arr = [12, 43, 19]; Array.prototype.index = 110;//此原型属性不会被打印出来 for(var value of arr) { console.log(value+'\n'); } 结果如下: 综述:foreach更多的用来遍历数组,但使用比较复杂;for in 一般常用来遍历 对象;for of 用来遍历数组非常方便且比较安全。 如果觉得写的不错,可以点个小小的赞或者关注我,如果有哪里写得不好,欢迎大家指出、

优秀的个人博客,低调大师

深入理解HBase的系统架构

HBase的构成 物理上来说,HBase是由三种类型的服务器以主从模式构成的。这三种服务器分别是:Region server,HBase HMaster,ZooKeeper。 其中Region server负责数据的读写服务。用户通过沟通Region server来实现对数据的访问。 HBase HMaster负责Region的分配及数据库的创建和删除等操作。 ZooKeeper作为HDFS的一部分,负责维护集群的状态(某台服务器是否在线,服务器之间数据的同步操作及master的选举等)。 另外,Hadoop DataNode负责存储所有Region Server所管理的数据。HBase中的所有数据都是以HDFS文件的形式存储的。出于使Region server所管理的数据更加本地化的考虑,Region server是根据DataNode分布的。HBase的数据在写入的时候都存储在本地。但当某一个region被移除或被重新分配的时候,就可能产生数据不在本地的情况。这种情况只有在所谓的compaction之后才能解决。 NameNode负责维护构成文件的所有物理数据块的元信息(metadata)。 HBase结构如下图所示: Regions HBase中的表是根据row key的值水平分割成所谓的region的。一个region包含表中所有row key位于region的起始键值和结束键值之间的行。集群中负责管理Region的结点叫做Region server。Region server负责数据的读写。每一个Region server大约可以管理1000个region。Region的结构如下图所示:HBase的HMaster HMaster负责region的分配,数据库的创建和删除操作。 具体来说,HMaster的职责包括: 调控Region server的工作 在集群启动的时候分配region,根据恢复服务或者负载均衡的需要重新分配region。 监控集群中的Region server的工作状态。(通过监听zookeeper对于ephemeral node状态的通知)。 管理数据库 提供创建,删除或者更新表格的接口。 HMaster的工作如下图所示:ZooKeeper HBase利用ZooKeeper维护集群中服务器的状态并协调分布式系统的工作。ZooKeeper维护服务器是否存活,是否可访问的状态并提供服务器故障/宕机的通知。ZooKeeper同时还使用一致性算法来保证服务器之间的同步。同时也负责Master选举的工作。需要注意的是要保证良好的一致性及顺利的Master选举,集群中的服务器数目必须是奇数。例如三台或五台。 ZooKeeper的工作如下图所示:HBase各组成部分之间的合作 ZooKeeper用来协调分布式系统的成员之间共享的状态信息。Region Server及HMaster也与ZooKeeper连接。ZooKeeper通过心跳信息为活跃的连接维持相应的ephemeral node。如下图所示:每一个Region server都在ZooKeeper中创建相应的ephemeral node。HMaster通过监控这些ephemeral node的状态来发现正常工作的或发生故障下线的Region server。HMaster之间通过互相竞争创建ephemeral node进行Master选举。ZooKeeper会选出区中第一个创建成功的作为唯一一个活跃的HMaster。活跃的HMaster向ZooKeeper发送心跳信息来表明自己在线的状态。不活跃的HMaster则监听活跃HMaster的状态,并在活跃HMaster发生故障下线之后重新选举,从而实现了HBase的高可用性。 如果Region server或者HMaster不能成功向ZooKeeper发送心跳信息,则其与ZooKeeper的连接超时之后与之相应的ephemeral node就会被删除。监听ZooKeeper状态的其他节点就会得到相应node不存在的信息,从而进行相应的处理。活跃的HMaster监听Region Server的信息,并在其下线后重新分配Region server来恢复相应的服务。不活跃的HMaster监听活跃HMaster的信息,并在起下线后重新选出活跃的HMaster进行服务。 HBase的第一次读写 HBase中有一个特殊的起目录作用的表格,称为META table。META table中保存集群region的地址信息。ZooKeeper中会保存META table的位置。 当用户第一次想HBase中进行读或写操作时,以下步骤将被执行: 1.客户从ZooKeeper中得到保存META table的Region server的信息。2.客户向该Region server查询负责管理自己想要访问的row key的所在的region的Region server的地址。客户会缓存这一信息以及META table所在位置的信息。3.客户与负责其row所在region的Region Server通信,实现对该行的读写操作。 在未来的读写操作中,客户会根据缓存寻找相应的Region server地址。除非该Region server不再可达。这时客户会重新访问META table并更新缓存。这一过程如下图所示:HBase的META table META table中保存了HBase中所有region的信息。 META table的格式类似于B tree。 META table的结构如下: 键:region的起始键,region id。 值:Region server如下图所示: Region Server的组成 运行在HDFS DataNode上的Region server包含如下几个部分: WAL:既Write Ahead Log。WAL是HDFS分布式文件系统中的一个文件。WAL用来存储尚未写入永久性存储区中的新数据。WAL也用来在服务器发生故障时进行数据恢复。 Block Cache:Block cache是读缓存。Block cache将经常被读的数据存储在内存中来提高读取数据的效率。当Block cache的空间被占满后,其中被读取频率最低的数据将会被杀出。 MemStore:MemStore是写缓存。其中存储了从WAL中写入但尚未写入硬盘的数据。MemStore中的数据在写入硬盘之前会先进行排序操作。每一个region中的每一个column family对应一个MemStore。 Hfiles:Hfiles存在于硬盘上,根据排序号的键存储数据行。Region server的结构如下图所示: **HBase的写操作步骤 步骤一** 当HBase的用户发出一个PUT请求时(也就是HBase的写请求),HBase进行处理的第一步是将数据写入HBase的write-ahead log(WAL)中。 WAL文件是顺序写入的,也就是所有新添加的数据都被加入WAL文件的末尾。WAL文件存在硬盘上。 当server出现问题之后,WAL可以被用来恢复尚未写入HBase中的数据(因为WAL是保存在硬盘上的)。如下图所示: 步骤二 当数据被成功写入WAL后,HBase将数据存入MemStore。这时HBase就会通知用户PUT操作已经成功了。 过程如下图所示:HBase的MemStore Memstore存在于内存中,其中存储的是按键排好序的待写入硬盘的数据。数据也是按键排好序写入HFile中的。每一个Region中的每一个Column family对应一个Memstore文件。因此对数据的更新也是对应于每一个Column family。 如下图所示:HBase Region Flush 当MemStore中积累了足够多的数据之后,整个Memcache中的数据会被一次性写入到HDFS里的一个新的HFile中。因此HDFS中一个Column family可能对应多个HFile。这个HFile中包含了相应的cell,或者说键值的实例。这些文件随着MemStore中积累的对数据的操作被flush到硬盘上而创建。 需要注意的是,MemStore存储在内存中,这也是为什么HBase中Column family的数目有限制的原因。每一个Column family对应一个MemStore,当MemStore存满之后,里面所积累的数据就会一次性flush到硬盘上。同时,为了使HDFS能够知道当前哪些数据已经被存储了,MemStore中还保存最后一次写操作的序号。 每个HFile中最大的序号作为meta field存储在其中,这个序号标明了之前的数据向硬盘存储的终止点和接下来继续存储的开始点。当一个region启动的时候,它会读取每一个HFile中的序号来得知当前region中最新的操作序号是什么(最大的序号)。 如下图所示:HFile HBase中的键值数据对存储在HFile中。上面已经说过,当MemStore中积累足够多的数据的时候就会将其中的数据整个写入到HDFS中的一个新的HFile中。因为MemStore中的数据已经按照键排好序,所以这是一个顺序写的过程。由于顺序写操作避免了磁盘大量寻址的过程,所以这一操作非常高效。 如下图所示:HFile的结构 HFile中包含了一个多层索引系统。这个多层索引是的HBase可以在不读取整个文件的情况下查找数据。这一多层索引类似于一个B+树。 键值对根据键大小升序排列。 索引指向64KB大小的数据块。 每一个数据块还有其相应的叶索引(leaf-index)。 每一个数据块的最后一个键作为中间索引(intermediate index)。 根索引(root index)指向中间索引。 文件结尾指向meta block。因为meta block是在数据写入硬盘操作的结尾写入该文件中的。文件的结尾同时还包含一些别的信息。比如bloom filter及时间信息。Bloom filter可以帮助HBase加速数据查询的速度。因为HBase可以利用Bloom filter跳过不包含当前查询的键的文件。时间信息则可以帮助HBase在查询时跳过读操作所期望的时间区域之外的文件。 如下图所示:HFile的索引 HFile的索引在HFile被打开时会被读取到内存中。这样就可以保证数据检索只需一次硬盘查询操作。 如下图所示:HBase的读合并(Read Merge)以及读放大(Read amplification) 通过上面的论述,我们已经知道了HBase中对应于某一行数据的cell可能位于多个不同的文件或存储介质中。比如已经存入硬盘的行位于硬盘上的HFile中,新加入或更新的数据位于内存中的MemStore中,最近读取过的数据则位于内存中的Block cache中。所以当我们读取某一行的时候,为了返回相应的行数据,HBase需要根据Block cache,MemStore以及硬盘上的HFile中的数据进行所谓的读合并操作。 1.HBase会首先从Block cache(HBase的读缓存)中寻找所需的数据。2.接下来,HBase会从MemStore中寻找数据。因为作为HBase的写缓存,MemStore中包含了最新版本的数据。3.如果HBase从Block cache和MemStore中没有找到行所对应的cell所有的数据,系统会接着根据索引和bloom filter从相应的HFile中读取目标行的cell的数据。 如下图所示:这里一个需要注意的地方是所谓的读放大效应(Read amplification)。根据前文所说,一个MemStore对应的数据可能存储于多个不同的HFile中(由于多次的flush),因此在进行读操作的时候,HBase可能需要读取多个HFile来获取想要的数据。这会影响HBase的性能表现。 如下图所示:HBase的Compaction Minor Compaction HBase会自动选取一些较小的HFile进行合并,并将结果写入几个较大的HFile中。这一过程称为Minor compaction。Minor compaction通过Merge sort的形式将较小的文件合并为较大的文件,从而减少了存储的HFile的数量,提升HBase的性能。 这一过程如下图所示:Major Compaction 所谓Major Compaction指的是HBase将对应于某一个Column family的所有HFile重新整理并合并为一个HFile,并在这一过程中删除已经删除或过期的cell,更新现有cell的值。这一操作大大提升读的效率。但是因为Major compaction需要重新整理所有的HFile并写入一个HFile,这一过程包含大量的硬盘I/O操作以及网络数据通信。这一过程也称为写放大(Write amplification)。在Major compaction进行的过程中,当前Region基本是处于不可访问的状态。 Major compaction可以配置在规定的时间自动运行。为避免影响业务,Major compaction一般安排在夜间或周末进行。 需要注意的一点事,Major compaction会将当前Region所服务的所有远程数据下载到本地Region server上。这些远程数据可能由于服务器故障或者负载均衡等原因而存储在于远端服务器上。 这一过程如下图所示:Region的分割(Region split) 首先我们快速复习一下Region: HBase中的表格可以根据行键水平分割为一个或几个region。每个region中包含了一段处于某一起始键值和终止键值之间的连续的行键。 每一个region的默认大小为1GB。 相应的Region server负责向客户提供访问某一region中的数据的服务。 每一个Region server能够管理大约1000个region(这些region可能来自同一个表格,也可能来自不同的表格)。 如下图所示:每一个表格最初都对应于一个region。随着region中数据量的增加,region会被分割成两个子region。每一个子region中存储原来一半的数据。同时Region server会通知HMaster这一分割。出于负载均衡的原因,HMaster可能会将新产生的region分配给其他的Region server管理(这也就导致了Region server服务远端数据的情况的产生)。 如下图所示:读操作的负载均衡(Read Load Balancing) Region的分割最初是在Region server本地发生的。但是出于负载均衡的原因,HMaster可能会将新产生的region分配给其他的Region server进行管理。这也就导致了Region server管理存储在远端服务器上的region情况的产生。这一情况会持续至下一次Major compaction之前。如上文所示,Major compaction会将任何不在本地的数据下载至本地。 也就是说,HBase中的数据在写入时总是存储在本地的。但是随着region的重新分配(由于负载均衡或数据恢复),数据相对于Region server不再一定是本地的。这种情况会在Major compaction后得到解决。 如下图所示:HDFS的数据备份(Data Replication) HDFS中所有的数据读写操作都是针对主节点进行的。HDFS会自动备份WAL和HFile。HBase以来HDFS来提供可靠的安全的数据存储。当数据被写入HDFS本地时,另外两份备份数据会分别存储在另外两台服务器上。 如下图所示:HBase的异常恢复(Crash Recovery) WAL文件和HFile都存储于硬盘上且存在备份,因此恢复它们是非常容易的。那么HBase如何恢复位于内存中的MemStore呢?当Region server宕机的时候,其所管理的region在这一故障被发现并修复之前是不可访问的。ZooKeeper负责根据服务器的心跳信息来监控服务器的工作状态。当某一服务器下线之后,ZooKeeper会发送该服务器下线的通知。HMaster收到这一通知之后会进行恢复操作。 HMaster会首先将宕机的Region server所管理的region分配给其他仍在工作的活跃的Region server。然后HMaster会将该服务器的WAL分割并分别分配给相应的新分配的Region server进行存储。新的Region server会读取并顺序执行WAL中的数据操作,从而重新创建相应的MemStore。 如下图所示:数据恢复(Data Recovery) WAL文件之中存储了一系列数据操作。每一个操作对应WAL中的一行。新的操作会顺序写在WAL文件的末尾。 那么当MemStore中存储的数据因为某种原因丢失之后应该如何恢复呢?HBase以来WAL对其进行恢复。相应的Region server会顺序读取WAL并执行其中的操作。这些数据被存入内存中当前的MemStore并排序。最终当MemStore存满之后,这些数据被flush到硬盘上。 如下图所示:Apache HBase的优缺点 优点 强一致性模型 当一个写操作得到确认时,所有的用户都将读到同一个值。 可靠的自动扩展 当region中的数据太多时会自动分割。 使用HDFS分布存储并备份数据。 内置的恢复功能 使用WAL进行数据恢复。 与Hadoop集成良好 MapReduce在HBase上非常直观。 缺点 WAL回复较慢。 异常恢复复杂且低效。 需要进行占用大量资源和大量I/O操作的Major compaction。 推荐阅读:大数据高手实战系列课程-深入剖析Hbase内核原理,互联网应用最佳实践 文章来源:https://blog.csdn.net/zougfang/article/details/80623997

优秀的个人博客,低调大师

用中文传话游戏理解 RNN

循环神经网络(RNN)是人工神经网络中非常流行的一个版本,其可以很好地处理序列数据类型。该数据类型指的是将一组数据点以特定顺序排列,从而使相关的数据点一个挨着一个,具体的例子有:股票市场价格、音频和视频数据、DNA 序列、传感器数据、自然语言文本等等。 为了直观地了解 RNN 是如何工作的,假设我们正在玩一个很受欢迎的儿童游戏——中文传话。该游戏的目的是最后一个人能准确预测出句子中所漏掉的词语,比如「The brown dog has four _____」。 中文传话游戏 玩家们需要排成一队,并且根据句子中词语的排列,每个人都会得到其中的一个词语,比如玩家 1 得到词语「the」,玩家 2 得到词语「brown」,玩家 3 得到词语「dog」,以此类推。首先,队伍中的第一个人把他听到的词语悄悄地告诉队伍中站在他身后的玩家。然后这个玩家

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册