开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring之CROS解决AJAX跨域问题

发表于 2017-11-10

说明

  1. 出于安全考虑,浏览器禁止AJAX调用驻留在当前来源之外的资源。例如,当您在一个标签中检查您的银行帐户时,您可以将evil.com网站放在另一个标签中。evil.com的脚本不能使用您的凭据向您的银行API发出AJAX请求(从您的帐户中提款)!
  2. 跨原始资源共享(CORS)是大多数浏览器实现的W3C规范,允许您以灵活的方式指定什么样的跨域请求被授权,而不是使用一些不太安全和不太强大的黑客,如IFrame或JSONP。
  3. Spring Framework 4.2 GA为开箱即用的CORS提供了一流的支持,为您提供了比典型的基于过滤器的解决方案更简单和更强大的配置方式。

Spring MVC提供了高级配置功能

控制器方法CORS配置

  • 您可以向[@RequestMapping](https://github.com/RequestMapping)注释处理程序方法添加[@CrossOrigin](https://github.com/CrossOrigin)注释,以便启用CORS(默认情况下[@CrossOrigin](https://github.com/CrossOrigin)允许[@RequestMapping](https://github.com/RequestMapping)注释中指定的所有起始和HTTP方法):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码  @RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}

}

  • 也可以为整个控制器启用CORS:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码  @CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

在此示例中,对于这两种方法retrieve()和remove()处理程序方法都启用了CORS支持,还可以看到如何使用[@CrossOrigin](https://github.com/CrossOrigin)属性自定义CORS配置。

您甚至可以同时使用控制器和方法级CORS配置,然后Spring将组合两个注释属性来创建合并的CORS配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin(origins = "http://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

如果您使用Spring Security,请确保在Spring Security级别启用CORS,并允许它利用Spring MVC级别定义的配置。

1
2
3
4
5
6
7
8
复制代码@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()...
}
}

全局CORS配置

除了细粒度的基于注释的配置,您也可能想要定义一些全局CORS配置。这与使用过滤器类似,但可以使用Spring MVC声明并结合细粒度[@CrossOrigin](https://github.com/CrossOrigin)配置。默认情况下,所有的起源和GET,HEAD和POST方法都是允许的。

JavaConfig

为整个应用程序启用CORS类似于

1
2
3
4
5
6
7
8
9
复制代码    @Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}

如果您使用的是Spring Boot,建议只要声明一个WebMvcConfigurerbean如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Configuration
public class MyConfiguration {

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}

您可以轻松地更改任何属性,以及仅将此CORS配置应用于特定路径模式:

1
2
3
4
5
6
7
8
9
复制代码@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(3600);
}

如果您使用Spring Security,请确保在Spring Security级别启用CORS,并允许它利用Spring MVC级别定义的配置。

XML命名空间

也可以使用mvc XML命名空间配置CORS 。

这种最小的XML配置使得/**路径模式上的CORS 具有与JavaConfig相同的默认属性:

1
2
3
复制代码<mvc:cors>
<mvc:mapping path="/**" />
</mvc:cors>

也可以使用自定义属性声明几个CORS映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<mvc:cors>

<mvc:mapping path="/api/**"
allowed-origins="http://domain1.com, http://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="false"
max-age="123" />

<mvc:mapping path="/resources/**"
allowed-origins="http://domain1.com" />

</mvc:cors>

如果您使用Spring Security,请不要忘记在Spring Security级别启用CORS:

1
2
3
4
5
复制代码<http>
<!-- Default to Spring MVC's CORS configuration -->
<cors />
...
</http>

它是如何工作的?

CORS请求(包括带有OPTIONS方法的预检)请求被自动发送到已HandlerMapping注册的各种。他们处理CORS预检要求和拦截CORS简单而实际的请求得益于
CorsProcessor实现(DefaultCorsProcessor以添加相关CORS响应头(如默认情况下)Access-Control-Allow-Origin)。
CorsConfiguration允许您指定如何处理CORS请求:允许的起点,头,方法等。它可以以各种方式提供:

  1. AbstractHandlerMapping#setCorsConfiguration()允许在路径模式上映射Map几个CorsConfiguration来指定一个/api/**
  2. 子类可以CorsConfiguration通过重写AbstractHandlerMapping#getCorsConfiguration(Object, HttpServletRequest)方法提供自己的子类
  3. 处理程序可以实现CorsConfigurationSource接口(像ResourceHttpRequestHandler现在这样),以便为每个请求提供CorsConfiguration。

基于过滤器的CORS支持

作为上述其他方法的替代方法,Spring Framework还提供了一个CorsFilter。在这种情况下,而不是使用[@CrossOrigin](https://github.com/CrossOrigin)或者WebMvcConfigurer#addCorsMappings(CorsRegistry)您可以例如在Spring
Boot应用程序中声明过滤器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@Configuration
public class MyConfiguration {

@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于非阻塞Java的数据库中间件--实践,问题和解决 II

发表于 2017-11-09

背景介绍

饿了么数据访问层(DAL)是基于 IO.Netty 实现的高并发、高吞吐量的 Java 服务。为了追求高性能在客户端连接和 DB 连接都使用了异步 IO 处理 SQL 请求和结果集。

关于DAL的线程模型以及他们介绍这里就不再赘述了,有兴趣的同学可以阅读下上一篇文章基于非阻塞Java的数据库中间件–实践,问题和解决。

本文主要介绍DAL在开发过程中关于Netty OOM的坑。

不期而遇的堆外OOM

介于堆外内存管理复杂性,我们的策略是避开直接使用堆外内存,由Netty框架自己处理堆外内存的申请与释放。

Netty里有四种主力的ByteBuf:UnpooledHeapByteBuf,UnpooledDirectByteBuf,PooledHeapByteBuf,PooledDirectByteBuf。顾名思义,Unpooled的对象就不需要太关心了, 能够依赖JVM GC自然回收。而PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要依赖引用技术器和回收过程。使用池化对象的时候,必须自己管理计数技术,是不是有一种熟悉的C的调调。

我们在Netty eventloop线程拿到消息后直接保存到堆内内存中,在我们的自己的worker线程处理业务逻辑,最后通过Netty Unpooled工具封装消息交给nettty eventloop线程继续工作。

1
复制代码sqlSessionContext.clientWriteAndFlush(Unpooled.wrappedBuffer(authen.toPacket())

考虑到Netty缓存池的频频爆出的内存泄漏问题,我们也小心翼翼的避免使用池化缓存池,看到源代码里默认是关闭池化内存的,也是让我们松了一口气。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//注 4.1以后官方对池化内存信心大增,默认为打开
String allocType = SystemPropertyUtil.get("io.netty.allocator.type", "unpooled").toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: unpooled (unknown: {})", allocType);
}

另外对于异常我们做了足够的考虑,无论读写出现任何异常,直接关闭对应的netty channel。

然而堆外内存OOM还是不期而至。一台服务器跑了21天后,吃光了所有内存与swap,生生的把自己变成了流量黑洞。

躲不开的堆外内存池

Netty的写入过程可以划分为两个基本步骤write与flush.

write的过程是将“数据请求”添加到ChannelOutboundBuffer,这个buffer是和每个socket具体绑定的。“数据请求”采用链的方式一一相接,在添加时候并无容量控制。
flush是将ChannelOutboundBuffer中的一批数据请求拿出来消费,即拷入socket的sendbuffer。能写入多少数据,则移除多少“数据请求”, 如果没有写完,并不会移除ChannelOutboundBuffer中的数据。

在我们调用netty write方法的时候有一个步骤是校验写数据是否为堆外内存形式。


虽然是使用池化内存的选项是“unpooled”,Netty还是非常”贴心”地帮我们默认使用了专门用于写数据的简易的堆外内存池。这个池子的使用规则是如果拿到的对象大小不够存放需要写的内容,则扩充这个bytebuf对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码static final class ThreadLocalUnsafeDirectByteBuf extends UnpooledUnsafeDirectByteBuf {

private static final Recycler<ThreadLocalUnsafeDirectByteBuf> RECYCLER =
new Recycler<ThreadLocalUnsafeDirectByteBuf>() {
@Override
protected ThreadLocalUnsafeDirectByteBuf newObject(Handle<ThreadLocalUnsafeDirectByteBuf> handle) {
return new ThreadLocalUnsafeDirectByteBuf(handle);
}
};

static ThreadLocalUnsafeDirectByteBuf newInstance() {
ThreadLocalUnsafeDirectByteBuf buf = RECYCLER.get();
buf.setRefCnt(1);
return buf;
}

对象总数大小

1
2
3
4
5
复制代码int maxCapacity = SystemPropertyUtil.getInt("io.netty.recycler.maxCapacity.default", 0);
if (maxCapacity <= 0) {
// TODO: Some arbitrary large number - should adjust as we get more production experience.
maxCapacity = 262144;
}

单个对象大小

1
2
复制代码THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 64 * 1024);
logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);

单个netty线程最大默认为262144 * 64 * 1024 = 16G 。

最终我们还是没有绕开netty的堆外内存缓存的坑。由于DAL是多租户的系统,线上复杂的sql使用情况,使得最终内存池在数量稳定后,逐渐趋向与最大对象对齐。


Netty 堆外内存的正确使用姿势


知道问题的原因,处理起来也就顺理成章了。我们需要选择一个合适缓存bytebuf大小,通过统计线上mysql消息的大小,我们选择缓存对象最大值为2K。超过2k,在RECYCLER回收机制下,bytebuf使用结束了直接会被释放。

1
复制代码-Dio.netty.threadLocalDirectBufferSize=2048

在性能测试中我们也尝试过完全关闭池子(设置-Dio.netty.threadLocalDirectBufferSize=0),然而Java堆外内存申请确实消耗非常大,立马成为了我们的瓶颈,线程容易卡在堆外内存的申请与释放上。


其次,我们需要限制堆外内存对象数量的总数。主要面对的问题是mysql大结果集问题,我们在这里通过设置AutoRead做流控。由于AutoRead在epoll的边缘触发模式下失效,在建立连接的需要确认使用水平触发模式。

1
2
3
4
5
复制代码Bootstrap b = new Bootstrap();
b.group(AthenaEventLoopGroupCenter.getSeverWorkerGroup());
b.channel(AthenaEventLoopGroupCenter.getChannelClass());
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(EpollChannelOption.EPOLL_MODE, EpollMode.LEVEL_TRIGGERED);

每次发送64K内容后我们先关闭autoread开关,等到客户端接受所有消息后再打开autoread开关。

1
2
3
4
5
复制代码/**
* Sets if {@link ChannelHandlerContext#read()} will be invoked automatically so that a user application doesn't
* need to call it at all. The default value is {@code true}.
*/
ChannelConfig setAutoRead(boolean autoRead);

另外需要谈到的是,Netty Channel的write 以及flush方法其实是反直觉的,它们并不会帮你把多次write的内容合并一个堆外内存对象,而是每次write就会生成一个堆外内存对象。针对mysql协议可能一次反回大量行数而整体数据量很小的情况(如查询某个表所有id列),就会催生大量的对象。

因此我们自己做了一次聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public class WriteBuf {
private final List<ByteBuf> bufs = new LinkedList<ByteBuf>();
private int bufSize;

public int write(ByteBuf buf) {
bufs.add(buf);
bufSize += buf.readableBytes();
return bufSize;
}

public ByteBuf readall() {
try {
return new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, bufs.size(), bufs);
} finally {
bufSize=0;
bufs.clear();
}
}
}

后记

我们一路走来,确实遇到了不少的坑,但不得不说Netty还是非常给力的。目前我们线上使用Zing JVM来解决GC停顿问题,配合Netty 的NIO模型,单机tps一般跑在7w,平均延迟在0.3ms内。

参考文档

Netty系列之Netty高性能之道

netty/netty

Netty: Home

作者介绍

林静 ,2011毕业于浙江大学,2015年加入饿了么,现任饿了么框架工具部架构师。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊一聊Spring中的线程安全性

发表于 2017-11-06

Spring与线程安全


Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。

Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。

  • singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。
  • prototype:bean被定义为在每次注入时都会创建一个新的对象。
  • request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。
  • session:bean被定义为在一个session的生命周期内创建一个单例对象。
  • application:bean被定义为在ServletContext的生命周期中复用一个单例对象。
  • websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。

无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题),用完即关(或交还给连接池)。

有人可能会认为,我使用request作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误的,因为Controller默认是单例的,一个HTTP请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把Controller的scope改成prototype,实际上Struts2就是这么做的,但有一点要注意,Spring MVC对请求的拦截粒度是基于每个方法的,而Struts2是基于每个类的,所以把Controller设为多例将会频繁的创建与回收对象,严重影响到了性能。

通过阅读上文其实已经说的很清楚了,Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。

下面将通过解析ThreadLocal的源码来了解它的实现与作用,ThreadLocal是一个很好用的工具类,它在某些情况下解决了线程安全问题(在变量不需要被多个线程共享时)。

本文作者为SylvanasSun(sylvanas.sun@gmail.com),首发于SylvanasSun’s Blog。
原文链接:sylvanassun.github.io/2017/11/06/…
(转载请务必保留本段声明,并且保留超链接。)

ThreadLocal


ThreadLocal是一个为线程提供线程局部变量的工具类。它的思想也十分简单,就是为线程提供一个线程私有的变量副本,这样多个线程都可以随意更改自己线程局部的变量,不会影响到其他线程。不过需要注意的是,ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。

ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。

ThreadLocal中含有一个叫做ThreadLocalMap的内部类,该类为一个采用线性探测法实现的HashMap。它的key为ThreadLocal对象而且还使用了WeakReference,ThreadLocalMap正是用来存储变量副本的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码    /**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
....
}

ThreadLocal中只含有三个成员变量,这三个变量都是与ThreadLocalMap的hash策略相关的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码    /**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode();

/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

唯一的实例变量threadLocalHashCode是用来进行寻址的hashcode,它由函数nextHashCode()生成,该函数简单地通过一个增量HASH_INCREMENT来生成hashcode。至于为什么这个增量为0x61c88647,主要是因为ThreadLocalMap的初始大小为16,每次扩容都会为原来的2倍,这样它的容量永远为2的n次方,该增量选为0x61c88647也是为了尽可能均匀地分布,减少碰撞冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码        /**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

要获得当前线程私有的变量副本需要调用get()函数。首先,它会调用getMap()函数去获得当前线程的ThreadLocalMap,这个函数需要接收当前线程的实例作为参数。如果得到的ThreadLocalMap为null,那么就去调用setInitialValue()函数来进行初始化,如果不为null,就通过map来获得变量副本并返回。

setInitialValue()函数会去先调用initialValue()函数来生成初始值,该函数默认返回null,我们可以通过重写这个函数来返回我们想要在ThreadLocal中维护的变量。之后,去调用getMap()函数获得ThreadLocalMap,如果该map已经存在,那么就用新获得value去覆盖旧值,否则就调用createMap()函数来创建新的map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码    /**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

protected T initialValue() {
return null;
}

ThreadLocal的set()与remove()函数要比get()的实现还要简单,都只是通过getMap()来获得ThreadLocalMap然后对其进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码    /**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

getMap()函数与createMap()函数的实现也十分简单,但是通过观察这两个函数可以发现一个秘密:ThreadLocalMap是存放在Thread中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码    /**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// Thread中的源码

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

仔细想想其实就能够理解这种设计的思想。有一种普遍的方法是通过一个全局的线程安全的Map来存储各个线程的变量副本,但是这种做法已经完全违背了ThreadLocal的本意,设计ThreadLocal的初衷就是为了避免多个线程去并发访问同一个对象,尽管它是线程安全的。而在每个Thread中存放与它关联的ThreadLocalMap是完全符合ThreadLocal的思想的,当想要对线程局部变量进行操作时,只需要把Thread作为key来获得Thread中的ThreadLocalMap即可。这种设计相比采用一个全局Map的方法会多占用很多内存空间,但也因此不需要额外的采取锁等线程同步方法而节省了时间上的消耗。

ThreadLocal中的内存泄漏


我们要考虑一种会发生内存泄漏的情况,如果ThreadLocal被设置为null后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value会形成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数的源码为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
复制代码        /**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

// 清理key为null的Entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

在上文中我们发现了ThreadLocalMap的key是一个弱引用,那么为什么使用弱引用呢?使用强引用key与弱引用key的差别如下:

  • 强引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的强引用,如果不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。
  • 弱引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在之后调用set()、getEntry()和remove()函数时会清除所有key为null的Entry。

但要注意的是,ThreadLocalMap仅仅含有这些被动措施来补救内存泄漏问题。如果你在之后没有调用ThreadLocalMap的set()、getEntry()和remove()函数的话,那么仍然会存在内存泄漏问题。

在使用线程池的情况下,如果不及时进行清理,内存泄漏问题事小,甚至还会产生程序逻辑上的问题。所以,为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。

参考文献


  • Are Spring objects thread safe? - Stack Overflow
  • Spring Singleton, Request, Session Beans and Thread Safety | Java Enterprise Ecosystem.
  • Spring Framework Documentation

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【分享实录-猫眼电影】业务纵横捭阖背后的技术拆分与融合

发表于 2017-11-03

王洋:猫眼电影商品业务线技术负责人、技术专家。主导了猫眼商品供应链和交易体系从0到1的建设,并在猫眼与美团拆分、与点评电影业务融合过程中,从技术层面保障了商品业务的平稳切换,同时也是美团点评《领域驱动设计》课程的讲师。在加入猫眼电影之前,曾就职于蚂蚁金服,参与了阿里网商银行从0到1的建设,以及支付宝钱包、花呗等产品的研发。

导读:互联网电影行业在2016年经历了较大的变动,其中包括猫眼电影和原美团的拆分,以及猫眼电影和点评电影业务的融合。业务发生大的变化时,技术通常也会做出较大的重构,猫眼后台技术团队在整个拆分、融合的过程中,对系统架构、领域模型进行了比较多的调整和思考,探索出一套成本和收益较为平衡的技术方案。本文将分享实践的具体过程、步骤和方法,希望给后互联网时代,遭遇业务拆分和融合的技术团队提供一些参考案例。

一.问题的提出

具有一定规模的互联网公司,通常会有很多的细分领域和垂直业务,为了提高效率,大部分公司都会采用平台化的思路:建设一套通用的基础平台,所有业务线基于这套基础平台搭建自己的业务和技术服务。这种做法一方面可以缩减人力成本,另一方面也可以提高新业务的开发效率。猫眼的商品业务一开始为了快速发展,就采用了这种发展模式,将整个业务的交易都搭建在美团的团购体系之上,如图1所示:

从上图可以看出,商品业务完全依赖于美团的商品、订单、支付、促销、劵服务,耦合性非常高。由于当时美团和猫眼是同一家公司,这种耦合性是可以接受的。但后来猫眼独立了,组织架构、业务规划出现了较大的不同,这种耦合性就变成了商品业务后续发展的一大难题。

基于独立发展的考虑,猫眼需要将商品业务从团购体系拆分出来,并和点评电影的商品业务融合在一起,组建一套新的业务和技术体系,用来支撑后续的业务发展。

经过大量的现状分析和调研之后,团队确定了此次技术升级的目标:

● 产品需求不能中断。
● 最低的客户感知度。
● 新旧体系可以自由切换。
● 最低的试错成本。
● 持续分阶段交付、验证。

电商体系从供应链到交易再到结算,复杂度已经很高,再加上业务切换、数据整合的时间,必然会是一场持久战。为了减少风险,让商务、财务等团队更好地配合我们,技术团队必须分阶段产出,做线上验证。

确定上述的5个目标之后,接下来我们就要思考如何围绕这些目标设计整体的方案。

二.技术拆分方案

首先,我们来分析一下业务场景,图2是商品业务的简图:

从图2可以看出,整个业务可以被拆解为四个大的模块,这样我们就粗略地确定了团队的分工:供应链、交易、消费、消费后服务。确定好团队的组成,接下来我们需要对服务的重要性和优先级做一个明确的划分。分析的维度如下:

● 基础数据层面,如商家、用户、合同,这些是公司长期积累下的资源,短期内不可能重建,所以这个层面的服务,尽量复用以前的服务。
● 核心服务层面,供应链的核心是上单,交易的核心是商品、订单、支付、促销,消费的核心是物流、商品券和与之对应的网关服务,这些都是交易的主流程,必须重建。
● 财务层面,如对接商家的结算,以及反应业务运转情况的账务,在公司拆分时,通常是需要独立核算的,所以这个层面的服务也必须重建。
● 非功能性服务,例如客服、售后,这些服务通常优先级不高,但是没有的话,会影响用户的体验,所以尽量考虑复用以前的服务,后期再考虑重建。
经过上面的分析,我们确定了一期工程需要重建的服务。

2.1 拆分的前置方案

确定了需要重建的服务之后,还不能急于开发,要提前思考如果所有的服务都已经有了,该如何做切换。需要考虑的事情有这么几点:

● 切换期间,商品如何售卖。

在前面我们确立了“新旧体系可自由切换”的目标,所以在切换过程中,需要保证商家和运营上一次单,就可以在美团、点评、猫眼三个体系中售卖,这样可以提高切换的灵活性,减少切换带来的交易损失。

● 版本问题。

前文已经提到,商品业务一开始是在团购之上搭建起来的,客户端调用的大部分接口都是美团团购的接口。最直观的做法是在团购接口中加上判断,将电影品类的交易请求导流到猫眼这边,这种做法速度快,且没有流量损失,但以后修改接口参数、扩展业务的负担比较重,可维护性比较差。

● 切换的方式和粒度。

为了达成“新旧体系可自由切换”和“最低试错成本”的目标,在切换过程中,必然会处于一个新老并存的状态,所以我们不能简单地替换接口,而要采用更灵活的切换方式和更细的切换粒度。

● 与猫眼现有业务的关系。

猫眼本身有一套选座交易,但受制于行业模型,暂时无法支撑商品交易。考虑到未来肯定是需要融合的,所以需要提前考虑两套交易的关系。

2.1.1 上单双推

针对售卖问题,猫眼采用了“上单双推”的做法,如图3:

正常情况下,上单系统在确认商家提交的商品信息后,会将商品推送到商品中心。为了让商品在美团、点评、猫眼三个体系中售卖,猫眼需要做以下几步操作:

● 历史单迁移。

先从美团商品中心将当下可以售卖的商品同步到猫眼,保持对齐。

● 建立关系映射。

对于商家来说,无论商品在几个体系中售卖,都是一个商品,在统计销量、结算时,都应该是一个商品,所以我们需要将多个商品中心的商品关联起来。这个做起来比较简单,只要给猫眼的商品分配一个id,然后在商品数据中存下对方的id就可以了。

另外,为了区分两个体系的商品,我们采用分段的策略来分配商品id,比如当下的美团商品最大id是X,那么猫眼的商品就采用2×X作为id的起始,而在美团商品id增长到2*X之前,我们肯定完成了切换,不用再考虑id序列统一的问题了。

● 商品双推,库存按比例分配。

猫眼上单系统确认商品信息后,将商品推送到美团、猫眼两个商品中心,并创建各自的库存,根据当下的交易比例分配库存量。由于美团、点评两个体系是经过融合的,所以只需要推送美团团购,点评就可以售卖了。

2.1.2 业务入口回收

解决了售卖问题之后,接下来我们来考虑版本和切换方式的问题。

众所周知,移动App一旦发版就很难再改动代码了,所以切换完成后,老的版本很难支持现有的体系。但经过分析我们发现,猫眼涉及的几个App,2~3个版本可以覆盖到90%以上的客户端,而核心 服务自建足够支撑3个版本以上的迭代,这样全部切换完成的时候,可以把老版本的流量损失降到5%以内(老版本的用户交易意愿有一定折损),这是一个可以接受的范围。

为了加快切换的速度,我们需要在核心服务自建之前,先把交易和消费环节所有的业务入口,也就是客户端调用的接口全部替换成猫眼自己的接口,由猫眼来对接美团的服务,如图4所示:

在美团的服务之上,先建立一个业务流程层,给客户端提供交易和消费环节所需要的接口,然后由业务流程层转接美团团购的基础服务。例如下单操作,原先是直接调用美团订单的接口,现在由交易业务系统做转接,一旦猫眼的订单系统开发完成,只需要将美团订单服务替换成猫眼订单服务就可以实现无缝对接了。

2.1.3 切换方法和粒度

收回业务入口以后,接下来需要考虑切换的粒度。

目前有这么几个粒度:商品、影院、App、入口位置(场次页、支付页、取票机、搜索、推荐)。综合起来看,影院+入口位置是比较合理的粒度。

商品粒度太细,且数据是动态变化的,维护起来比较麻烦;App维度又太粗,一次性切换起来涉及的范围太大,且无法回滚;影院的粒度处于中间,数据变化小,流量也比较好控制,所以我们采用了影院作为主要的切换粒度。

另外,由于商品业务本身的特点比较碎片化,入口众多,还得对接美团、点评的搜索、推荐等服务,所以将入口位置作为辅助的切换粒度。

切换之前,先给所有的展示入口分配固定的渠道号,如图5所示。支付页是渠道1,取票机是渠道3,客户端在请求后台数据时,需要带上渠道号;然后商品业务系统将查询逻辑抽象成处理器,例如猫眼支付页处理器对接猫眼的商品中心,而美团的支付页处理器对接美团的商品中心,公共流程如排序、最低价计算,可以抽象到父类中。

当一个查询请求到达商品业务系统后,由渠道分发控制器根据渠道号选择对应的处理器,同时,渠道分发控制器需要询问渠道切换服务的建议,看看是走团购体系还是走猫眼体系。而我们只需要在渠道切换服务内部维护一个走猫眼体系的影院+渠道列表,然后用配置推送服务动态更新,就可以实现影院+入口位置两个维度的自由切换了。

通过以上的办法,后台系统已经在商品展示层面实现了两个体系的自由切换,但后续的交易流程还有很长,需要客户端能够分辨后续该使用哪些接口,才可以走到正确的交易流程。图6是客户端需要配合做的改动:

客户端需要维护两套交易接口的列表,一套走美团,一套走猫眼。后台接口在返回商品数据时,会带上商品是否猫眼的标记,如果为true,则后续流程都使用猫眼的交易接口;如果为false,则都使用美团的交易接口。等到切换完成,后台返回的标记全部变成true,客户端也可以考虑删除这段逻辑,直接使用猫眼的接口列表。

2.1.4 统一id服务

最后,考虑商品交易和已有业务的关系。

交易的核心是订单,而猫眼内部已经存在一套选座订单了,但这两者的模型差异很大,无法复用,所以只能选择自建订单,在订单号层面做好统一,降低未来数据融合的复杂度。

统一订单id,就需要一个统计id服务,如图7所示。每个交易业务在下单前,都从统一id服务获取订单号,然后再下单,这样可以保证整个公司的订单号分布在同一个递增序列下,降低促销、结算等系统融合的复杂度。

2.2 核心服务自建

完成了前置方案的设计,接下来要分析交易需要哪些核心服务,下面是商品业务的行为分析图:

从上图中可以看出,整个交易过程可以简单的划分为三个阶段:交易前(商品为核心)、交易中(订单为核心)、交易后(即消费过程,商品券为核心)。下面详细分析每个阶段的核心模型该如何设计。

2.2.1 商品输出模型

根据商品的作用,我们可以将商品信息的存在形式分成三个阶段:编辑阶段、使用阶段、业务聚合阶段。以下是这三个阶段的说明:

● 编辑阶段,即还未成形的商品。

这个阶段以上单流程为核心,维护商品的写模型,所以需要重点关注商品的增删改操作,并做好审核机制和操作记录。这个阶段的模型主要维护在上单系统里边。

● 使用阶段,即通过审核,允许售卖的商品。

这个阶段维护的是商品的基础信息,一部分信息是只读的,例如商品标题、价格、所属的门店等;另外一部分是可变的,例如库存量、销量、商品状态等。使用阶段的模型主要维护在商品中心里边。

● 业务聚合阶段,即整合其它信息以后的商品。

这个阶段维护的是商品的读模型,不能改变商品的任何信息,例如商品列表是聚合了多个商品信息,商品详情则是聚合了商品基础信息和促销信息。当外部系统,例如搜索等服务需要接入商品体系的时候,都应该输出业务聚合阶段的商品。这个阶段的模型主要维护在商品业务里边。

定义好商品的三个阶段以后,就可以根据每个系统使用商品的方式来组织系统的关系,得到图9所展示的输出模型。

2.2.2 订单处理模型

订单是交易过程中最重要的模型之一,也是最容易和业务耦合过重的模块。所以在设计订单模型的时候,需要重点考虑如何让订单和业务流程分离,只关注订单的基本信息和状态流转。考虑点主要有以下几点:

● 命令模型和查询操作需要分开,即CQRS。

例如下单和查询订单详情两个操作,前者是交易的主流程,会写入订单的信息,而后者不会改变订单的任何信息,只需要查询到订单即可。另外,下单重视的是业务流程,核心指标是稳定性和准确性,而查询订单详情重视的是数据聚合,核心指标是完整性和用户体验。

● 业务流程和订单处理过程需要分离。

例如下单操作可能会分为好几个步骤:判断商品是否可售、计算商品的价格、锁库存、写订单数据等。这些步骤是业务流程,订单不应该关心。而且每个业务的交易流程可能会不一样,所以需要一个灵活性更高的办法来处理交易流程。

考虑以上两个问题后,我们设计出了订单处理模型,如图10所示。

首先得开发一个任务处理引擎,负责处理业务流程中的单个任务,例如锁库存、生成商品券、推送订单信息等等。每一个任务都对应一条数据库记录,用来说明要使用哪个处理器、用到的数据该从哪里获取,以及步骤完成之后该怎么办,记录之间也会使用编号连接起来,用来处理优先级和依赖关系。

接单服务负责将不同业务的下单流程拆解成一个个子任务,并确定好任务之间的优先级和依赖关系,进行简单的编排,然后写入到任务引擎的数据库中,而任务引擎则负责捞取这些任务,分析并执行已经编排过的任务。这种方式的好处在于业务流程被拆得很细,不同业务之间可以重用一些任务处理步骤,当一个新的交易业务接入的时候,只需要添加对应的子任务处理器,然后在接单系统中配置编排的规则,即可快速支持。

如图10所示,我们将订单的写模型和查模型分开,让增删改三个操作都走订单核心系统的写模型,而用户查询操作则走订单查询系统的查模型。每次订单信息有变化的时候,订单核心会通知任务引擎,由任务引擎负责更新查模型。这期间会有时间上的延迟,所以订单查询维护的查模型只能给用户端展示需求使用,而退款过程中查询订单状态则必须走订单核心的写模型,因为写模型的状态是实时的。

用户成功支付订单以后,订单核心会收到支付成功消息,然后将不同类型的订单拆分,并挨个通知任务引擎,执行支付成功后的业务流程,例如生成商品券、推送订单信息到美团订单列表等。

2.2.3 商品券与结算模型

商品券和物流是消费过程中的核心模型,而结算刚好要的就是消费维度的数据,所以这两者的设计紧密相关。由于商品券和物流比较类似,所以本文只介绍商品券的模型。
商品券的本质是一串数字,用来作为兑换服务的凭证。它的特点:

● 一是要足够乱,不能让你随便输入一个数字就可以使用;
● 二是不能重复,否则无法分清对应的是哪个服务。

在数据操作上,商品券需要支持生成、验证、撤销等操作。
经过以上分析,我们设计出了商品券和结算的模型,如图11:

为了足够乱,系统需要一个随机数生成的模块,同时为了不重复,需要为随机数建立一个防冲突表,每次生成完随机数,都需要到防冲突表里查一下是否有使用过。由于随机数是有限的,生成的码越多,冲突的概率就越高,对并发度会有一定的影响。为了优化这个问题,我们采用了异步生成的方式:先预生成一定数量的劵码,并添加到一个可用劵码的队列中,在不同的并发数下,只要调整队列的容量和预生成速度就可以支持了。

当用户通过pos机等设备验证券码的时候,商品券会发消息给结算系统,结算系统会写一条流水数据到数据库中,并按照结算周期聚合到一起形成付款计划。结算系统也会每天扫描合同中约定的结算周期,触发打款操作。

2.3 线上线下切换

在前文介绍的前置方案里,我们已经提前将切换点埋入到了客户端中,当交易模型搭建起来时,线上实际已经有大量的客户端支持在两个体系切换了。但此时,我们还不能着急将所有的商家都切换到新体系,需要考虑更多的问题:

● 旧体系已有的业务,在新体系是否都已经支持。例如促销、优惠券,必须在业务对齐的情况下才能切换,否则会干扰到商家的正常运营活动。
● 商家结算的模式是否支持切换。在拆分过程中,结算、财务方面的数据是唯一不能迁移的数据,任何的变化都需要两个公司财务之间核算清楚才能执行。所以在切换之前,需要根据结算的模式,将商家做好分类,优先将结算方式简单的商家切换到新体系,用于验证线上服务的正确性和切换方案的合理性。
● 交易过程中是否依赖第三方系统。例如猫眼在交易后的过程中,依赖了影院的第三方券系统,导致了这部分影院必须在对接完依赖的第三系统之后,才可以切换。
分析完上述三个因素,确定好可以切换的商家列表之后,就可以开始和商务一起推进线上线下的切换了。切换的过程中,需要注意线下的切换通常比线上要缓慢,所以要预先启动线下的切换,并让商务团队给商家做好培训。

三.技术融合方案

经过技术拆分,猫眼实际拥有了一套完整的商品供应链和交易体系,而此时点评的商品业务也有一套自己的供应链和交易。为了降低今后的维护成本,我们必须将点评的商品业务适配到猫眼的这套供应链和交易体系之上。

供应链层面的解决方案和拆分的方案类似,这里不再详述,重点说说交易层面的适配和融合办法。设计融合技术方案的时候,需要重点考虑这么几个问题:

● 客户端调用接口的迁移问题。点评客户端之前对接的都是点评的业务服务,需要替换成猫眼的业务服务。
● 交易流程衔接以及页面跳转的问题。融合之后,核心的如订单、支付、券消费肯定得走猫眼的交易体系,所以整个交易的页面和跳转都应该使用猫眼的页面。
● 已经有的数据整合问题。在融合之前,点评已经积累了大量的商品订单和券数据,这部分数据需要和猫眼的数据进行整合,才能满足用户和商家的查询需求。

3.1 业务入口适配

针对客户端调用接口的问题,解决思路和拆分的方案差不多,也是提前建立业务层,将客户端使用的接口都回收到业务系统,然后再适配到猫眼的业务服务,如图12所示。

为了尽量不维护两套业务系统,猫眼在融合一期工程的时候先采用了简单的适配,目的是将业务入口先牵引到猫眼的交易体系,然后在后续的开发中再让点评客户端直接对接猫眼的业务系统,逐步取代点评的适配层,等到需要适配的版本越来越少的时候,就可以废弃掉这个适配层了。

3.2 交易过程使用触屏版

交易流程的衔接和跳转是个比较棘手的问题,一是猫眼以前没有在点评客户端做过开发,为了融合单做一套native交易页面的代价较高;二是今后在做功能迭代的时候,要适配的端太多,会拖慢产品迭代的速度。综合考虑了流量、体验和团队多方面的因素之后,猫眼决定使用触屏版来解决这个问题。

商品交易有一个特点,即大部分的下单操作都会先经过商品详情页,所以只要从商品详情页开始,做一套触屏版页面,就可以将交易流程都引导到猫眼的页面了。

然而商品的展示通常比较碎片化,可能会有搜索、推荐、商品列表、猜你喜欢等各种入口,所以进入交易的第一步就是尽可能的让展示入口都跳转到触屏版的商品详情页,然后继续下单、支付,再跳转到触屏版的结果页。订单成功之后,猫眼再将自己的订单数据 推送到点评的订单中心,同时将订单详情页的跳转设置成触屏版。一旦用户点击订单列表,就又回到了猫眼可控的范围内,后续的退款、消费、客服就走回到猫眼的交易体系,具体的做法如图13所示。

整个过程中,可能会遇到一些其它的问题,例如跳转登录:点评客户端登陆的是点评账号,而猫眼的触屏版登陆的是猫眼账号,所以这个做法还依赖于账号的融合。在跳转收银台的时候也需要小心谨慎,需要点评收银台和猫眼收银台采用同样的验证方式和解析规则,否则极容易出现金额错误,或者无法支付的情况。这些也是触屏版需要考虑的问题。

使用触屏版的前提是:交易流程必须经过商品详情页,假如交易流程不经过商品详情页的话,就必须对多个业务入口进行适配,但跳转到收银台之后的流程,仍然是可以复用的。

3.3 数据整合思路

数据整合是技术融合中最繁琐的部分,做法通常有三种:

● 数据层面不做整合,在代码层面区分该走哪个数据源。这个方法的优势在于数据可不做改动,但需要维护两套数据源,成本较高。
● 数据层面建立映射关系,然后通过转换层将数据转换成业务方需要的模型。这个方法的改动成本比较可控,可以快速实现两个体系的互通。
● 数据层面做彻底的整合,将两方的数据迁移到一个数据源中。这个方法的优势是只保留一套数据模型,后期可维护性高,但整合的难度大,需要重点处理差异数据。

猫眼和点评数据的整合过程中,主要以后两种方法为主,如图14所示:

查询数据通常使用的都是id,而我们可以简单的将数据id分为两类:一类是不能变化的,例如影院id;一类是可以变化的,例如订单id。

对于影院id,猫眼和点评都有自己的一套序列,而且这两套id都依附在各自的商家体系上,不能轻易统一起来,所以只能通过建立映射关系的方式来实现互通。图14中左半部分的示例就是解决数据id不能改变的场景:

先将点评的影院id和猫眼的影院id建立映射关系,例如猫眼的影院id=1和点评的影院id=2,可能指向同一家影院(只是举例使用,不保证一定是对应关系)。而猫眼app和点评app依然使用之前的id,只是在查询数据的时候,会先经过数据转换层,由数据转换分析映射关系,拿到统一之后的影院信息。

对于订单id,用户通常不会手动记录订单id是多少,完全依靠后台返回,这时候就可以将点评的订单数据按照猫眼的格式,迁移到猫眼的数据库中,并将id转换成猫眼序列的id,字段如果不同的话,可以使用扩展字段或者差异表做一层兼容。

图14的右半部分描述 的就是这种做法:用户在查询订单列表的时候,返回的是猫眼的订单id,那么进入到详情的时候自然就会查询猫眼的订单数据,这样就实现了数据整合。

需要注意的是:修改订单id的时候,通常需要修改所有和订单相关的数据,例如促销、商品券等系统,都是以订单id为区分依据的,这就需要同时改动所有受牵连的数据,需要小心谨慎地去推进。

四、案例启示和教训

技术拆分和融合的过程在猫眼内部持续将近半年时间,期间遇到了不少困难,对系统架构和模型也进行了较多的思考,以下是从案例中获得的启示:

● 在大公司做垂直业务时,如果要复用平台的服务,最好在客户端和平台服务之间建立业务层,做一层服务转接,这样可以为服务的替换提供更好的灵活性。
● 在设计系统模型时,尽量让业务入口渠道化、处理过程组件化,这样不仅可以提高系统的横向扩展性,而且也可以对每个入口做更精细的把控
● 不同业务在快速发展时,可以有自己的核心服务,但必须使用统一的id生成策略,保证模型的主要id属于同一个递增序列,这样可以为以后的融合,或者平台化减少数据整合的复杂度。
● 数据模型层面,尽量将命令模型和查询模型分开,一方面可以将主流程和数据展示操作彻底分开;一方面也可以降低数据操作的复杂性。
● 系统的流量控制和切换粒度要足够精细,这样可以提高应对风险的能力。

也从本案例中积累了一些教训:

● 设计技术拆分和融合方案时,需要全面考虑非技术因素的影响,比如结算、财务,数据既不能迁移,也不能修改,此时财务的结论很有可能会影响整体的切换方案和模式设计。
● 线下的切换要提前进行,以便尽早收集到业务一线的反馈,可以并行的去修复问题,这样才能保证在线上开始切换的时候,不被拖慢进度。

五、总结和感谢

技术拆分和融合是一个庞大的工程,涉及的技术点比较多,由于篇幅有限,本文只是介绍了整体的设计方案,希望给行业中遭遇相同问题的工程师们提供一个参考案例。

最后,在此感谢所有参与本次技术拆分和融合的技术、产品、运营和商务、财务团队所有的小伙伴,以及美团点评热心帮助的同事们。有了大家的紧密协作,才能在有限的时间里,完成如此复杂的技术升级。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Vuex 源码解析

发表于 2017-10-30

写在前面

因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。

文章的原地址:github.com/answershuto…。

在学习过程中,为Vue加上了中文的注释github.com/answershuto…以及Vuex的注释github.com/answershuto…,希望可以对其他想学习源码的小伙伴有所帮助。

可能会有理解存在偏差的地方,欢迎提issue指出,共同学习,共同进步。

Vuex

我们在使用Vue.js开发复杂的应用时,经常会遇到多个组件共享同一个状态,亦或是多个组件会去更新同一个状态,在应用代码量较少的时候,我们可以组件间通信去维护修改数据,或者是通过事件总线来进行数据的传递以及修改。但是当应用逐渐庞大以后,代码就会变得难以维护,从父组件开始通过prop传递多层嵌套的数据由于层级过深而显得异常脆弱,而事件总线也会因为组件的增多、代码量的增大而显得交互错综复杂,难以捋清其中的传递关系。

那么为什么我们不能将数据层与组件层抽离开来呢?把数据层放到全局形成一个单一的Store,组件层变得更薄,专门用来进行数据的展示及操作。所有数据的变更都需要经过全局的Store来进行,形成一个单向数据流,使数据变化变得“可预测”。

Vuex是一个专门为Vue.js框架设计的、用于对Vue.js应用程序进行状态管理的库,它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,以一个单例存放,同时利用Vue.js的响应式机制来进行高效的状态管理与更新。正是因为Vuex使用了Vue.js内部的“响应式机制”,所以Vuex是一个专门为Vue.js设计并与之高度契合的框架(优点是更加简洁高效,缺点是只能跟Vue.js搭配使用)。具体使用方法及API可以参考Vuex的官网。

先来看一下这张Vuex的数据流程图,熟悉Vuex使用的同学应该已经有所了解。

Vuex实现了一个单向数据流,在全局拥有一个State存放数据,所有修改State的操作必须通过Mutation进行,Mutation的同时提供了订阅者模式供外部插件调用获取State数据的更新。所有异步接口需要走Action,常见于调用后端接口异步获取更新数据,而Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。Vuex运行依赖Vue内部数据双向绑定机制,需要new一个Vue对象来实现“响应式化”,所以Vuex是一个专门为Vue.js设计的状态管理库。

安装

使用过Vuex的朋友一定知道,Vuex的安装十分简单,只需要提供一个store,然后执行下面两句代码即完成的Vuex的引入。

1
2
3
4
5
6
7
复制代码Vue.use(Vuex);

/*将store放入Vue创建时的option中*/
new Vue({
el: '#app',
store
});

那么问题来了,Vuex是怎样把store注入到Vue实例中去的呢?

Vue.js提供了Vue.use方法用来给Vue.js安装插件,内部通过调用插件的install方法(当插件是一个对象的时候)来进行插件的安装。

我们来看一下Vuex的install实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码/*暴露给外部的插件install方法,供Vue.use调用安装插件*/
export function install (_Vue) {
if (Vue) {
/*避免重复安装(Vue.use内部也会检测一次是否重复安装同一个插件)*/
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
/*保存Vue,同时用于检测是否重复安装*/
Vue = _Vue
/*将vuexInit混淆进Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/
applyMixin(Vue)
}

这段install代码做了两件事情,一件是防止Vuex被重复安装,另一件是执行applyMixin,目的是执行vuexInit方法初始化Vuex。Vuex针对Vue1.0与2.0分别进行了不同的处理,如果是Vue1.0,Vuex会将vuexInit方法放入Vue的_init方法中,而对于Vue2.0,则会将vuexinit混淆进Vue的beforeCreacte钩子中。来看一下vuexInit的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 /*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
/*存在store其实代表的就是Root节点,直接执行store(function时)或者使用store(非function)*/
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
/*子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store*/
this.$store = options.parent.$store
}
}

vuexInit会尝试从options中获取store,如果当前组件是根组件(Root节点),则options中会存在store,直接获取赋值给$store即可。如果当前组件非根组件,则通过options中的parent获取父组件的$store引用。这样一来,所有的组件都获取到了同一份内存地址的Store实例,于是我们可以在每一个组件中通过this.$store愉快地访问全局的Store实例了。

那么,什么是Store实例?

Store

我们传入到根组件到store,就是Store实例,用Vuex提供到Store方法构造。

1
2
3
4
5
6
7
复制代码export default new Vuex.Store({
strict: true,
modules: {
moduleA,
moduleB
}
});

我们来看一下Store的实现。首先是构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
复制代码constructor (options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
/*
在浏览器环境下,如果插件还未安装(!Vue即判断是否未安装),则它会自动安装。
它允许用户在某些情况下避免自动安装。
*/
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

if (process.env.NODE_ENV !== 'production') {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `Store must be called with the new operator.`)
}

const {
/*一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)*/
plugins = [],
/*使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。*/
strict = false
} = options

/*从option中取出state,如果state是function则执行,最终得到一个对象*/
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}

// store internal state
/* 用来判断严格模式下是否是用mutation修改state的 */
this._committing = false
/* 存放action */
this._actions = Object.create(null)
/* 存放mutation */
this._mutations = Object.create(null)
/* 存放getter */
this._wrappedGetters = Object.create(null)
/* module收集器 */
this._modules = new ModuleCollection(options)
/* 根据namespace存放module */
this._modulesNamespaceMap = Object.create(null)
/* 存放订阅者 */
this._subscribers = []
/* 用以实现Watch的Vue实例 */
this._watcherVM = new Vue()

// bind commit and dispatch to self
/*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/
const store = this
const { dispatch, commit } = this
/* 为dispatch与commit绑定this(Store实例本身) */
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}

// strict mode
/*严格模式(使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误)*/
this.strict = strict

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
/*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
resetStoreVM(this, state)

// apply plugins
/* 调用插件 */
plugins.forEach(plugin => plugin(this))

/* devtool插件 */
if (Vue.config.devtools) {
devtoolPlugin(this)
}
}

Store的构造类除了初始化一些内部变量以外,主要执行了installModule(初始化module)以及resetStoreVM(通过VM使store“响应式”)。

installModule

installModule的作用主要是用为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
复制代码/*初始化module*/
function installModule (store, rootState, path, module, hot) {
/* 是否是根module */
const isRoot = !path.length
/* 获取module的namespace */
const namespace = store._modules.getNamespace(path)

// register in namespace map
/* 如果有namespace则在_modulesNamespaceMap中注册 */
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}

// set state
if (!isRoot && !hot) {
/* 获取父级的state */
const parentState = getNestedState(rootState, path.slice(0, -1))
/* module的name */
const moduleName = path[path.length - 1]
store.`_withCommit`(() => {
/* 将子module设置称响应式的 */
Vue.set(parentState, moduleName, module.state)
})
}

const local = module.context = makeLocalContext(store, namespace, path)

/* 遍历注册mutation */
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})

/* 遍历注册action */
module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})

/* 遍历注册getter */
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})

/* 递归安装mudule */
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}

resetStoreVM

在说resetStoreVM之前,先来看一个小demo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码let globalData = {
d: 'hello world'
};
new Vue({
data () {
return {
?state: {
globalData
}
}
}
});

/* modify */
setTimeout(() => {
globalData.d = 'hi~';
}, 1000);

Vue.prototype.globalData = globalData;

/* 任意模板中 */
<div>```{{globalData.d}}```</div>

上述代码在全局有一个globalData,它被传入一个Vue对象的data中,之后在任意Vue模板中对该变量进行展示,因为此时globalData已经在Vue的prototype上了所以直接通过this.prototype访问,也就是在模板中的。此时,setTimeout在1s之后将globalData.d进行修改,我们发现模板中的globalData.d发生了变化。其实上述部分就是Vuex依赖Vue核心实现数据的“响应式化”。

不熟悉Vue.js响应式原理的同学可以通过笔者另一篇文章响应式原理了解Vue.js是如何进行数据双向绑定的。

接着来看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
复制代码/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
/* 存放之前的vm对象 */
const oldVm = store._vm

// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}

/* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})

// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
Vue.config.silent = true
/* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
store._vm = new Vue({
data: {
?state: state
},
computed
})
Vue.config.silent = silent

// enable strict mode for new vm
/* 使能严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store)
}

if (oldVm) {
/* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.?state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}

resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法,这样我们就可以在组件里访问this.$store.getter.test就等同于访问store._vm.test。

1
2
3
4
5
6
7
8
复制代码forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})

之后Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。

1
2
3
4
5
6
复制代码store._vm = new Vue({
data: {
?state: state
},
computed
})

这时候我们访问store._vm.test也就访问了Vue实例中的属性。

这两步执行完以后,我们就可以通过this.$store.getter.test访问vm中的test属性了。

严格模式

Vuex的Store构造类的option有一个strict的参数,可以控制Vuex执行严格模式,严格模式下,所有修改state的操作必须通过mutation实现,否则会抛出错误。

1
2
3
4
5
6
7
8
9
复制代码/* 使能严格模式 */
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.?state }, () => {
if (process.env.NODE_ENV !== 'production') {
/* 检测store中的_committing的值,如果是true代表不是通过mutation的方法修改的 */
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}

首先,在严格模式下,Vuex会利用vm的$watch方法来观察?state,也就是Store的state,在它被修改的时候进入回调。我们发现,回调中只有一句话,用assert断言来检测store._committing,当store._committing为false的时候会触发断言,抛出异常。

我们发现,Store的commit方法中,执行mutation的语句是这样的。

1
2
3
4
5
复制代码this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})

再来看看_withCommit的实现。

1
2
3
4
5
6
7
复制代码_withCommit (fn) {
/* 调用withCommit修改state的值时会将store的committing值置为true,内部会有断言检查该值,在严格模式下只允许使用mutation来修改store中的值,而不允许直接修改store的数值 */
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}

我们发现,通过commit(mutation)修改state数据的时候,会再调用mutation方法之前将committing置为true,接下来再通过mutation函数修改state中的数据,这时候触发$watch中的回调断言committing是不会抛出异常的(此时committing为true)。而当我们直接修改state的数据时,触发$watch的回调执行断言,这时committing为false,则会抛出异常。这就是Vuex的严格模式的实现。

接下来我们来看看Store提供的一些API。

commit(mutation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码/* 调用mutation的commit方法 */
commit (_type, _payload, _options) {
// check object-style commit
/* 校验参数 */
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)

const mutation = { type, payload }
/* 取出type对应的mutation的方法 */
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
/* 执行mutation中的所有方法 */
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
/* 通知所有订阅者 */
this._subscribers.forEach(sub => sub(mutation, this.state))

if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}

commit方法会根据type找到并调用_mutations中的所有type对应的mutation方法,所以当没有namespace的时候,commit方法会触发所有module中的mutation方法。再执行完所有的mutation之后会执行_subscribers中的所有订阅者。我们来看一下_subscribers是什么。

Store给外部提供了一个subscribe方法,用以注册一个订阅函数,会push到Store实例的_subscribers中,同时返回一个从_subscribers中注销该订阅者的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码/* 注册一个订阅函数,返回取消订阅的函数 */
subscribe (fn) {
const subs = this._subscribers
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}

在commit结束以后则会调用这些_subscribers中的订阅者,这个订阅者模式提供给外部一个监视state变化的可能。state通过mutation改变时,可以有效补获这些变化。

dispatch(action)

来看一下dispatch的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码/* 调用action的dispatch方法 */
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)

/* actions中取出type对应的ation */
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}

/* 是数组则包装Promise形成一个新的Promise,只有一个则直接返回第0个 */
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}

以及registerAction时候做的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码/* 遍历注册action */
function registerAction (store, type, handler, local) {
/* 取出type对应的action */
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
/* 判断是否是Promise */
if (!isPromise(res)) {
/* 不是Promise对象的时候转化称Promise对象 */
res = Promise.resolve(res)
}
if (store._devtoolHook) {
/* 存在devtool插件的时候触发vuex的error给devtool */
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}

因为registerAction的时候将push进_actions的action进行了一层封装(wrappedActionHandler),所以我们在进行dispatch的第一个参数中获取state、commit等方法。之后,执行结果res会被进行判断是否是Promise,不是则会进行一层封装,将其转化成Promise对象。dispatch时则从_actions中取出,只有一个的时候直接返回,否则用Promise.all处理再返回。

watch

1
2
3
4
5
6
7
复制代码/* 观察一个getter方法 */
watch (getter, cb, options) {
if (process.env.NODE_ENV !== 'production') {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
}
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}

熟悉Vue的朋友应该很熟悉watch这个方法。这里采用了比较巧妙的设计,_watcherVM是一个Vue的实例,所以watch就可以直接采用了Vue内部的watch特性提供了一种观察数据getter变动的方法。

registerModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */
registerModule (path, rawModule) {
/* 转化称Array */
if (typeof path === 'string') path = [path]

if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(path.length > 0, 'cannot register the root module by using registerModule.')
}

/*注册*/
this._modules.register(path, rawModule)
/*初始化module*/
installModule(this, this.state, path, this._modules.get(path))
// reset store to update getters...
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
resetStoreVM(this, this.state)
}

registerModule用以注册一个动态模块,也就是在store创建以后再注册模块的时候用该接口。内部实现实际上也只有installModule与resetStoreVM两个步骤,前面已经讲过,这里不再累述。

unregisterModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码 /* 注销一个动态module */
unregisterModule (path) {
/* 转化称Array */
if (typeof path === 'string') path = [path]

if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}

/*注销*/
this._modules.unregister(path)
this._withCommit(() => {
/* 获取父级的state */
const parentState = getNestedState(this.state, path.slice(0, -1))
/* 从父级中删除 */
Vue.delete(parentState, path[path.length - 1])
})
/* 重制store */
resetStore(this)
}

同样,与registerModule对应的方法unregisterModule,动态注销模块。实现方法是先从state中删除模块,然后用resetStore来重制store。

resetStore

1
2
3
4
5
6
7
8
9
10
11
12
复制代码/* 重制store */
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}

这里的resetStore其实也就是将store中的_actions等进行初始化以后,重新执行installModule与resetStoreVM来初始化module以及用Vue特性使其“响应式化”,这跟构造函数中的是一致的。

插件

Vue提供了一个非常好用的插件Vue.js devtools

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码/* 从window对象的__VUE_DEVTOOLS_GLOBAL_HOOK__中获取devtool插件 */
const devtoolHook =
typeof window !== 'undefined' &&
window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
if (!devtoolHook) return

/* devtoll插件实例存储在store的_devtoolHook上 */
store._devtoolHook = devtoolHook

/* 出发vuex的初始化事件,并将store的引用地址传给deltool插件,使插件获取store的实例 */
devtoolHook.emit('vuex:init', store)

/* 监听travel-to-state事件 */
devtoolHook.on('vuex:travel-to-state', targetState => {
/* 重制state */
store.replaceState(targetState)
})

/* 订阅store的变化 */
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
}

如果已经安装了该插件,则会在windows对象上暴露一个VUE_DEVTOOLS_GLOBAL_HOOK。devtoolHook用在初始化的时候会触发“vuex:init”事件通知插件,然后通过on方法监听“vuex:travel-to-state”事件来重置state。最后通过Store的subscribe方法来添加一个订阅者,在触发commit方法修改mutation数据以后,该订阅者会被通知,从而触发“vuex:mutation”事件。

最后

Vuex是一个非常优秀的库,代码量不多且结构清晰,非常适合研究学习其内部实现。最近的一系列源码阅读也使我自己受益匪浅,写这篇文章也希望可以帮助到更多想要学习探索Vuex内部实现原理的同学。

关于

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: github.com/answershuto

Blog:answershuto.github.io/

知乎主页:www.zhihu.com/people/cao-…

知乎专栏:zhuanlan.zhihu.com/ranmo

掘金: juejin.cn/user/289926…

osChina:my.oschina.net/u/3161824/b…

转载请注明出处,谢谢。

欢迎关注我的公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

项目开发框架-SSM 1Spring 2Spring M

发表于 2017-10-28

1.Spring

无需多言,作为开源届数一数二的典例,项目开发中无处不在;
核心IOC容器,用来装载bean(java中的类)-用Spring的IOC容器来管理Bean的生命周期,有了这样一种机制,我们就可以不用在代码中去重复的做new操作。
aop,面向切面编程,spring中最主要的是用于事务方面的使用。

2.Spring MVC

作用于web层,相当于controller,与struts中的action一样,都是用来处理用户请求的。同时,相比于struts2来说,更加细粒度,它是基于方法层面的,而struts是基于类层面的。

3.MyBatis

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。[来自:www.mybatis.org/mybatis-3/z…]

他人总结

  • Hibernate功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度很快,非常爽。
  • Hibernate的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行。
  • MYBATIS入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。
  • MYBATIS的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。4.SSM框架整合

本项目将以购物为背景,主要包括商品信息及库存【因为想顺便学习一下事务的处理】、订单信息。下面将从数据库创建、项目结构说明、配置文件、业务代码等方面进行一步步说明。4.1 数据库创建

1.商品表

1
2
3
4
5
6
复制代码CREATE TABLE `goods` (
`goods_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`goodsname` varchar(100) NOT NULL COMMENT '商品名称',
`number` int(11) NOT NULL COMMENT '商品库存',
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='商品表'

初始化表数据

1
2
复制代码INSERT INTO `goods` (`goods_id`, `goodsname`, `number`)
VALUES (1001, 'SN卫衣', 15)

2.订单表

1
2
3
4
5
6
7
8
复制代码CREATE TABLE `orderinfo` (
`order_id` varchar(20) NOT NULL COMMENT '订单编号',
`goods_id` bigint(18) NOT NULL COMMENT '商品ID',
`user_id` bigint(10) NOT NULL COMMENT '用户ID',
`order_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '下单时间' ,
PRIMARY KEY (`order_id`),
INDEX `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表'

OK,至此表结构及初始化数据构建完成,下面说下基于Mavan的项目结构。项目结构说明

因为项目是使用maven来管理jar包的,先来贴一下,pom.xml的配置

  • pom.xml
    为了避免学习小伙伴崇尚拿来主义【也就是去除了xmlns之类的东西】,这里只放项目依赖的jar包的dependencies;本案例将本着“需则用”的原则,避免在网上看到的各种乱七八糟的依赖都丢进来的情况,造成资源浪费和干扰阅读。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
复制代码<dependencies>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>

<!-- 1.日志 slf4j-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.1</version>
</dependency>

<!-- 2.数据库连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
<scope>runtime</scope>
</dependency>
<!-- 2.数据库连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>

<!-- 3.MyBatis 以及 spring-mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.3</version>
</dependency>

<!-- 4.Servlet 相关依赖 -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

<!-- 5.Spring -->

<!-- 5.1 Spring核心 :core bean context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<!-- 5.2 Spring jdbc依赖,事务依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<!-- 5.3 Spring web依赖>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<!-- 5.4 Spring test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>

<!-- 6.redis客户端:Jedis【不使用的话可以直接去除】 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.8</version>
</dependency>

<!-- 7.工具类 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2</version>
</dependency>
</dependencies>

*项目结构图

src/test/java:用于junit的测试类src/main/java:
dao:数据库处理
service:业务处理
enums:项目枚举
mapper:dao中方法对应mybatis映射文件,Sql就在这里面
web:控制器,controller
entity:项目中的实体类,如:商品类和订单类
配置文件


  • jdbc.properties
1
2
3
4
复制代码jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://serverName:port/dbname?useUnicode=true&characterEncoding=utf8
jdbc.username=[填写自己的数据库用户名]
jdbc.password=[填写自己的数据库登录密码]
+ logback.xml  
这里直接用的是控制台输出,如果是生产环境,可以根据具体的需求进行配置。
1
2
3
4
5
6
7
8
9
10
11
12
复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
+ mybatis-config 这里主要是MyBaties全局配置文件的配置,可以将一些类的别名、主键自增配置、驼峰命名规则配置等。
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<configuration>
<!-- 配置全局属性 -->
<settings>
<!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true" />

<!-- 使用列别名替换列名 默认:true -->
<setting name="useColumnLabel" value="true" />

<!-- 开启驼峰命名转换:Table{create_time} -> Entity{createTime} -->
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
</configuration>
+ spring 相关配置文件 为了更加清晰的了解spring各个组件的作用,这里将数据源的配置、事务配置和视图解析器的配置分开来。 **spring-dao.xml** 这里面主要就是spring配置整合mybatis的具体过程,具体包括: 1.引入数据库配置文件 2.配置数据源【数据库连接池】 3.配置SqlSessionFactory对象 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码<!-- 1.配置数据库相关参数properties的属性:${url} -->
<context:property-placeholder location="classpath:jdbc.properties" />

<!-- 2.数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />

<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30" />
<property name="minPoolSize" value="10" />
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false" />
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="10000" />
<!-- 当获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2" />
</bean>

<!-- 3.配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml" />
<!-- 扫描entity包 使用别名 -->
<property name="typeAliasesPackage" value="com.glmapper.framerwork.entity" />
<!-- 扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="com.glmapper.framerwork.mapper/*.xml" />
</bean>

<!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!-- 给出需要扫描Dao接口包 -->
<property name="basePackage" value="com.glmapper.framerwork.dao" />
</bean>
+ spring-service 实际的开发过程中事务一般都是在service层进行操作。因此用一个单独的spring-service.xml来进行事务的相关的配置
1
2
3
4
5
6
7
8
9
10
复制代码<!-- 扫描service包下所有使用注解的类型 -->
<context:component-scan base-package="com.glmapper.framerwork.service" />
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 配置基于注解的声明式事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
+ spring-web.xml 配置SpringMVC;需要说明一下,一般我们在实际的开发过程中,会配置json2map解析。这里没有用到就不贴出来,读者可以自行网上搜索一波。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码<!-- 1.开启SpringMVC注解模式 -->
<mvc:annotation-driven />
<!-- 2.静态资源默认servlet配置
(1)加入对静态资源的处理:js,css,图片等
(2)允许使用"/"做整体映射
-->
<mvc:default-servlet-handler/>

<!-- 3.配置视图解析器ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>

<!-- 4.扫描web相关的bean -->
<context:component-scan base-package="com.glmapper.framerwork.web" />
+ web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码<!-- 编码过滤器 -->  
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 防止Spring内存溢出监听器 -->
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置springMVC需要加载的配置文件
spring-dao.xml,spring-service.xml,spring-web.xml
Mybatis - > spring -> springmvc
-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<!-- 默认匹配所有的请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
至此,所有的配置文件结束,下面将进行具体的代码环节业务代码 ---- 这里mapper中的xml文件就不贴了,自行脑补。。。。 + 实体类:包括商品和订单

商品类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码/**
* 商品信息类
* @author glmapper
*
*/
public class Goods {
private long goodsId;// 商品ID
private String goodsName;// 商品名称
private int number;// 商品库存

public long getGoodsId() {
return goodsId;
}
public void setGoodsId(long goodsId) {
this.goodsId = goodsId;
}
public String getGoodsName() {
return goodsName;
}
public void setGoodsName(String goodsName) {
this.goodsName = goodsName;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}

订单类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码/**
* 订单信息类
* @author glmapper
*
*/
public class OrderInfo {
private String orderId;//订单ID
private long goodsId;//商品ID
private long userId;//用户ID
private Date orderTime;//下单时间
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public long getGoodsId() {
return goodsId;
}
public void setGoodsId(long goodsId) {
this.goodsId = goodsId;
}
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public Date getOrderTime() {
return orderTime;
}
public void setOrderTime(Date orderTime) {
this.orderTime = orderTime;
}
}
  • 商品dao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码public interface GoodsDao {

/**
* 通过ID查询单件商品信息
*
* @param id
* @return
*/
Goods queryById(long id);

/**
* 查询所有商品信息
*
* @param offset 查询起始位置
* @param limit 查询条数
* @return
*/
List<Goods> queryAll(@Param("offset") int offset, @Param("limit") int limit);

/**
* 减少商品库存
*
* @param bookId
* @return 如果影响行数等于>1,表示更新的记录行数
*/
int reduceNumber(long goodsId);

}
  • 订单dao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public interface OrderInfoDao {

/**
* 插入订单记录
*
* @param OrderInfo orderInfo
* @return 插入的行数
*/
int insertOrderInfo(OrderInfo orderInfo);

/**
* 通过主键查询订单记录,返回订单实体
* @param orderId
* @return
*/
OrderInfo queryByOrderId(String orderId);
}
  • 下单服务接口orderService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
复制代码@Service("orderService")
public class OrderServiceImpl implements OrderService {
//log生成器
private Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

// 注入dao依赖【商品dao,订单dao】
@Autowired
private GoodsDao goodsDao;
@Autowired
private OrderInfoDao orderInfoDao;

@Override
public Goods getById(long goodsId) {
// TODO Auto-generated method stub
return goodsDao.queryById(goodsId);
}

@Override
public List<Goods> getList(int offset,int limit) {
// TODO Auto-generated method stub
return goodsDao.queryAll(offset, limit);
}

@Override
@Transactional
public OrderInfo buyGoods(long goodsId, long userId) {
//扣减库存,插入订单 =一个事务 如果失败则执行回滚
try {
// 减库存
int update = goodsDao.reduceNumber(goodsId);
if (update <= 0) {// 库存不足
throw new NoNumberException("no number");
} else {
// 执行预约操作
OrderInfo orderInfo=new OrderInfo();
orderInfo.setGoodsId(goodsId);
orderInfo.setUserId(userId);
orderInfo.setOrderTime(new Date());
String orderId=getRandomOrderId(goodsId);
orderInfo.setOrderId(orderId);
int insert = orderInfoDao.insertOrderInfo(orderInfo);
if (insert <= 0) {// 重复预约
throw new RepeatAppointException("repeat appoint");
} else {// 预约成功
return orderInfo;
}
}
} catch (Exception e) {
//这里可以丰富下具体的返回信息
logger.error("下单失败");
}
return null;
}

private String getRandomOrderId(long goodsId) {
SimpleDateFormat dateFormater = new SimpleDateFormat("yyyyMMddhhmmss");
String prefix=dateFormater.format(new Date());
String goodsIdStr=goodsId+"";
String temp="";
for (int i = 0; i < 6; i++) {
Random random=new Random(goodsIdStr.length()-1);
temp+=goodsIdStr.charAt(random.nextInt());
}
return prefix+temp;
}
}

OK,至此所有核心代码及配置文件罗列完毕;【mapper中的xml和具体的controller就不贴了,相信大家对这个也不陌生。本文主要意图在于梳理下自己学习中的一些点,SSM框架在实际的应用开发中还会有很多其他的开源技术结合进来,如:quartz,redis等。当前本文的列子就是一个空壳子,以备参考吧】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Akka系列(八):Akka persistence设计理念

发表于 2017-10-26

这一篇文章主要是讲解Akka persistence的核心设计理念,也是CQRS(Command Query Responsibility Segregation)架构设计的典型应用,就让我们来看看为什么Akka persistence会采用CQRS架构设计。

CQRS

很多时候我们在处理高并发的业务需求的时候,往往能把应用层的代码优化的很好,比如缓存,限流,均衡负载等,但是很难避免的一个问题就是数据的持久化,以致数据库的性能很可能就是系统性能的瓶颈,我前面的那篇文章也讲到,如果我们用数据库去保证记录的CRUD,在并发高的情况下,让数据库执行这么多的事务操作,会让很多数据库操作超时,连接池不够用的情况,导致大量请求失败,系统的错误率上升和负载性能下降。

既然这样,那我们可不可借鉴一下读写分离的思想呢?假使写操作和同操作分离,甚至是对不同数据表,数据库操作,那么我们就可以大大降低数据库的瓶颈,使整个系统的性能大大提升。那么CQRS到底是做了什么呢?

我们先来看看普通的方式:

acid

acid

我们可以看出,我们对数据的请求都是通过相应的接口直接对数据库进行操作,这在并发大的时候肯定会对数据库造成很大的压力,虽然架构简单,但在面对并发高的情况下力不从心。

那么CQRS的方式有什么不同呢?我们也来看看它的执行方式:

cqrs

cqrs

乍得一看,似乎跟普通的方式没什么不同啊,不就多了一个事件和存储DB么,其实不然,小小的改动便是核心理念的转换,首先我们可以看到在CQRS架构中会多出一个Event,那它到底代表着什么含义呢?其实看过上篇文章的同学很容易理解,Event是我们系统根据请求处理得出的一个领域模型,比如一个修改余额操作事件,当然这个Event中只会保存关键性的数据。

很多同学又有疑问了,这不跟普通的读写分离很像么,难道还隐藏着什么秘密?那我们就来比较一下几种方式的不同之处:

1.单数据库模式
  • 写操作会产生互斥锁,导致性能降低;
  • 即使使用乐观锁,但是在大量写操作的情况下也会大量失败;
2.读写分离
  • 读写分离通过物理服务器增加,负荷增加;
  • 读写分离更适用于读操作大于写操作的场景;
  • 读写分离在面对大量写操作的情况下还是很吃力;
3.CQRS
  • 普通数据的持久化和Event持久化可以使用同一台数据库;
  • 利用架构设计可以使读和写操作尽可能的分离;
  • 能支撑大量写的操作情况;
  • 可以支持数据异步持久,确保数据最终一致性;

从三种方式各自的特点可以看出,单数据库模式的在大量读写的情况下有很大的性能瓶颈,但简单的读写分离在面对大量写操作的时候也还是力不从心,比如最常见的库存修改查询场景:

common-action

common-action

我们可以发现在这种模式下写数据库的压力还会很大,而且还有数据同步,数据延迟等问题。

那么我们用CQRS架构设计会是怎么样呢:

cqrs-action

cqrs-action

首先我们可以业务模型进行分离,对不同的查询进行分离,另外避免不了的同一区间数据段进行异步持久化,在保证数据一致性的情况下提升系统的吞吐量。这种设计我们很少会遇到事务竞争,另外还可以使用内存数据库(当然如果是内存操作那就最快)来提升数据的写入。(以上的数据库都可为分布式数据库,不担心单机宕机)

那么CRQS机制是怎么保证数据的一致性的呢?

从上图中我们可以看出,一个写操作我们会在系统进行初步处理后生成一个领域事件,比如a用户购买了xx商品1件,b用户购买了xx商品2件等,按照普通的方式我们肯定是直接将订单操作,库存修改操作一并放在一个事务内去操作数据库,性能可想而知,而用CQRS的方式后,首先系统在持久化相应的领域事件后和修改内存中的库存(这个处理非常迅速)后便可马上向用户做出反应,真正的具体信息持久可以异步进行,当然若是当在具体信息持久化的过程中出错了怎么办,系统能恢复正确的数据么,当然可以,因为我们的领域事件事件已经持久化成功了,在系统恢复的时候,我们可以根据领域事件来恢复真正的数据,当然为了防止恢复数据是造成数据丢失,数据重复等问题我们需要制定相应的原则,比如给领域事件分配相应id等。

使用CQRS会带来性能上的提升,当然它也有它的弊端:

  • 使系统变得更复杂,做一些额外的设计;
  • CQRS保证的是最终一致性,有可能只适用于特定的业务场景;

Akka Persistence 中CQRS的应用

通过上面的讲解,相信大家对CQRS已经有了一定的了解,下面我们就来看看它在Akka Persistence中的具体应用,这里我就结合上一篇文章抽奖的例子,比如其中的LotteryCmd便是一个写操作命令,系统经过相应的处理后得到相应的领域事件,比如其中LuckyEvent,然后我们将LuckyEvent进行持久化,并修改内存中抽奖的余额,返回相应的结果,这里我们就可以同时将结果反馈给用户,并对结果进行异步持久化,流程如下:

cqrs-example

cqrs-example

可以看出,Akka Persistence的原理完全是基于CQRS的架构设计的,另外Persistence Actor还会保存一个内存状态,相当于一个in memory数据库,可以用来提供关键数据的存储和查询,比如前面说到的库存,余额等数据,这部分的设计取决于具体的业务场景。

阅读Akka Persistence相关源码,其的核心就在于PersistentActor接口中的几个持久方法,比如其中的

1
2
3
复制代码def persist[A](event: A)(handler: A ⇒ Unit): Unit

def persistAll[A](events: immutable.Seq[A])(handler: A ⇒ Unit): Unit

等方法,它们都有两个参数,一个是持久化的事件,一个是持久化后的后续处理逻辑,我们可以在后续handler中修改Actor内部状态,向外部发送消息等操作,这里的模式就是基于CQRS架构的,修改状态有事件驱动,另外Akka还可以在系统出错时,利用相应的事件恢复Actor的状态。

总结

总的来说,CQRS架构是一种不同于以往的CRUD的架构,所以你在享受它带来的高性能的同时可能会遇到一些奇怪的问题,当然这些都是可以解决的,重要的是思维上的改变,比如事件驱动,领域模型等概念,不过相信当你理解并掌握它之后,你便会爱上它的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

究竟什么是可重入锁?

发表于 2017-10-23

经历

很久之前就听说了可重入锁,可重入锁究竟是什么意思,以前是囫囵吞枣的,只要记住ReentrantLock和sychronized是可重入锁就行了,爱咋用咋用,好吧,原谅我的无知,最近对基础查漏补缺,发现竟然对其一问三不知,赶紧预习一波,觉得有必要写一篇博客来讲解,就当做什么都没有发生吧,嘿嘿。。。

释义

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁,下面是一个用synchronized实现的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public class ReentrantTest implements Runnable {

public synchronized void get() {
System.out.println(Thread.currentThread().getName());
set();
}

public synchronized void set() {
System.out.println(Thread.currentThread().getName());
}

public void run() {
get();
}

public static void main(String[] args) {
ReentrantTest rt = new ReentrantTest();
for(;;){
new Thread(rt).start();
}
}
}

整个过程没有发生死锁的情况,截取一部分输出结果如下:

1
2
3
4
5
6
7
8
复制代码Thread-8492
Thread-8492
Thread-8494
Thread-8494
Thread-8495
Thread-8495
Thread-8493
Thread-8493

set()和get()同时输出了线程名称,表明即使递归使用synchronized也没有发生死锁,证明其是可重入的。

不可重入锁

不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码import java.util.concurrent.atomic.AtomicReference;

public class UnreentrantLock {

private AtomicReference<Thread> owner = new AtomicReference<Thread>();

public void lock() {
Thread current = Thread.currentThread();
//这句是很经典的“自旋”语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}

public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}

代码也比较简单,使用原子引用来存放线程,同一线程两次调用lock()方法,如果不执行unlock()释放锁的话,第二次调用自旋的时候就会产生死锁,这个锁就不是可重入的,而实际上同一个线程不必每次都去释放锁再来获取锁,这样的调度切换是很耗资源的。稍微改一下,把它变成一个可重入锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码import java.util.concurrent.atomic.AtomicReference;

public class UnreentrantLock {

private AtomicReference<Thread> owner = new AtomicReference<Thread>();
private int state = 0;

public void lock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
state++;
return;
}
//这句是很经典的“自旋”式语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}

public void unlock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
if (state != 0) {
state--;
} else {
owner.compareAndSet(current, null);
}
}
}
}

在执行每次操作之前,判断当前锁持有者是否是当前对象,采用state计数,不用每次去释放锁。

ReentrantLock中可重入锁实现

这里看非公平锁的锁获取方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码        final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//就是这里
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python学习笔记(进阶篇一)

发表于 2017-10-19

笔记整理出处:廖雪峰教程

  • 进阶
    • 函数
      • 位置参数
      • 默认参数
      • 可变参数
      • 关键字参数
      • 命名关键字参数
      • 参数组合
      • 递归函数
    • 高级特性
      • 切片
      • 迭代
      • 列表生成式
      • 生成器
      • 迭代器

进阶

函数

在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。

我们以自定义一个求绝对值的my_abs函数为例:

1
2
3
4
5
复制代码def my_abs(x):
if x >= 0:
return x
else:
return -x

请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。
return None可以简写为return。
python中函数没有返回值类型声明,同时,函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

1
2
3
复制代码>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1
位置参数

我们先写一个计算x2的函数:

1
2
复制代码def power(x):
return x * x

对于power(x)函数,参数x就是一个位置参数。

当我们调用power函数时,必须传入有且仅有的一个参数x:

默认参数
1
2
复制代码def power(x , y = 2):
return x * y

我们调用时既可以这样用power(2,3),也可以这样用power(2),明显的,当我们不传递y这个参数时,方法内部会去y的默认值进行运算,也就是2

默认参数可以简化函数的调用。设置默认参数时,有几点要注意:

  • 必选参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在必选参数前面);
  • 如何设置默认参数。
    当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

使用默认参数有什么好处?最大的好处是能降低调用函数的难度。因为有些参数,可能我们大部分时间传递的是同样的值。
注意事项:

  • 定义默认参数要牢记一点:默认参数必须指向不变对象!
  • 定义默认参数要牢记一点:默认参数必须指向不变对象!
  • 定义默认参数要牢记一点:默认参数必须指向不变对象!

举例说明,先定义一个函数,传入一个list,添加一个END再返回:

1
2
3
复制代码def add_end(L=[]):
L.append('END')
return L

当你正常调用时,结果似乎不错:

1
2
3
4
复制代码>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']

当你使用默认参数调用时,一开始结果也是对的:

1
2
复制代码>>> add_end()
['END']

但是,再次调用add_end()时,结果就不对了:

1
2
3
4
复制代码>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了’END’后的list。

原因解释如下:

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

所以,定义默认参数要牢记一点:默认参数必须指向不变对象!

要修改上面的例子,我们可以用None这个不变对象来实现:

1
2
3
4
5
复制代码def add_end(L=None):
if L is None:
L = []
L.append('END')
return L

现在,无论调用多少次,都不会有问题:

1
2
3
4
复制代码>>> add_end()
['END']
>>> add_end()
['END']

为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数

定义与java类似,基本使用方法如下:

1
2
3
4
5
复制代码def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

对于已经存在的list类型参数,可变参数的使用方法和java略有不同,不能直接传入该变量,需要增加*

1
2
3
复制代码>>> nums = [1, 2, 3]
>>> calc(*nums)
14

*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键字参数

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:

1
2
复制代码def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:

1
2
复制代码>>> person('Michael', 30)
name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

1
2
3
4
复制代码>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

1
2
3
复制代码>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的kw参数,kw将获得一个dict,

注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。

命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部检查。
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:

1
2
3
4
5
6
7
8
复制代码def person(name, age, *, city, job):
print(name, age, city, job)
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。

调用方式如下:
~~~python
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

1
2
复制代码def person(name, age, *args, city, job):
print(name, age, args, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

1
2
3
4
复制代码>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given

由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。

命名关键字参数可以有缺省值,从而简化调用:

1
2
3
4
5
6
复制代码def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由于命名关键字参数city具有默认值,调用时,可不传入city参数:
~~~python
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个作为特殊分隔符。如果缺少,Python解释器将无法识别位置参数和命名关键字参数:

1
2
3
复制代码def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass
参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

比如定义一个函数,包含上述若干种参数:

1
2
3
4
5
复制代码def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。

递归函数

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

高级特性

切片

对经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。
取前3个元素,用一行代码就可以完成切片:

1
2
复制代码>>> L[0:3]
['Michael', 'Sarah', 'Tracy']

前开后闭原则。默认从第一个开始取时可以省略不写0.
类似的,Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片:

1
2
3
4
复制代码>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']

记住倒数第一个元素的索引是-1。
支持间隔取值,比如前10个数,每两个取一个:

1
2
复制代码>>> L[:10:2]
[0, 2, 4, 6, 8]

所有数,每5个取一个:

1
2
复制代码>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

甚至什么都不写,只写[:]就可以原样复制一个list:

1
2
复制代码>>> L[:]
[0, 1, 2, 3, ..., 99]

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:

1
2
复制代码>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)

字符串’xxx’也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:

1
2
3
4
复制代码>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
迭代

只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:

1
2
3
4
5
6
7
复制代码>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
a
c
b

默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。
由于字符串也是可迭代对象,因此,也可以作用于for循环。

那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:

1
2
3
4
5
6
7
复制代码>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

1
2
3
4
5
6
复制代码>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C

上面的for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:

1
2
3
4
5
6
复制代码>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9

任何可迭代对象都可以作用于for循环,包括自定义的数据类型,只要符合迭代条件,就可以使用for循环。

列表生成式

如果要生成[1x1, 2x2, 3x3, …, 10x10]怎么做?方法一是循环:

1
2
3
4
5
6
复制代码>>> L = []
>>> for x in range(1, 11):
... L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

1
2
复制代码>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来。
还可以使用两层循环,可以生成全排列:

1
2
复制代码>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

鉴于列表生成式的便捷性,过于复杂的逻辑不建议直接使用生成式来写(个人观点)

生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

1
2
3
4
5
6
复制代码>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。
如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
...
81
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

我们讲过,generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象:

1
2
3
4
5
6
复制代码>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1

定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

1
2
3
4
5
6
7
8
9
10
11
复制代码def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

举个简单的例子,定义一个generator,依次返回数字1,3,5:

1
2
3
4
5
6
7
复制代码def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)

调用该generator时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

可以看到,odd不是普通函数,而是generator,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。

回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:

1
2
3
4
5
6
7
8
9
复制代码>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done
迭代器

可以直接作用于for循环的数据类型有以下几种:

一类是集合数据类型,如list、tuple、dict、set、str等;

一类是generator,包括生成器和带yield的generator function。

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。

可以使用isinstance()判断一个对象是否是Iterable对象:

1
2
3
4
5
复制代码>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance(100, Iterable)
False

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象:

1
2
3
4
5
复制代码>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
把list、dict、str等Iterable变成Iterator可以使用iter()函数
:

1
2
3
4
复制代码>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

翻译 服务性能监控:USE方法(The USE Meth

发表于 2017-10-19

原文链接: http://www.brendangregg.com/usemethod.html

原文作者:Bredan Gregg

翻译:小莞

校对:3D

USE 方法是一种能分析任何系统性能的方法论。我们可以根据能帮助系统分析的结构化清单,来迅速的定位资源的瓶颈和错误所在。它通常会先以列出问题为开始,然后再寻找适合的指标,而不是给你制定一些固定的指标,然后让你按部就班的执行下去。

本页左侧下方,是我列出的,根据不同的操作系统(Linux、Solaris等)衍生的USE方法列表。(译者注:可以参考原文链接)

我列出了为不同的操作系统而衍生的USE方法列表供大家参考,你们可以根据你的环境来为你的站点服务,选择适合的附加监控指标。

通过这个工具,可以很方便的筛选出适合不同的系统的建议 metrics:Rosetta(http://www.brendangregg.com/USEmethod/use-rosetta.html)

Intro(Introduction)

如果你遇到一个很严重的性能问题升级的时候,并且你不能确定它是否由服务导致的,这时候你该怎么办?

我们都说万事开头难。所以我开发出了 USE方法,来帮助大家,如何去快速的解决常见的性能问题,而同时又不容易忽略重要的地方。

USE方法在设计之初就定位了简洁、明了、完整、快速的特性,就好像一本航天手册的紧急事项列表那样。(译者注:航天手册,介绍包括不限于飞机的各种特性、指标、性能等,用于帮助飞行学员学习驾驶飞机,或者是帮助那些希望提高他们的飞行潜能和航空知识的人了解的更全面)。

USE方法已经在不同的企业、课堂(作为学习工具)以及最近的云计算等场景中,被成功应用了无数次。

USE方法基于 3+1 模型(三种指标类型+一种策略),来切入一个复杂的系统。我发现它仅仅发挥了 5% 的力量,就解决了大概 80% 的服务器问题,并且正如我将证明的,它除了服务器以外,也同样适应于各种系统。

它应当被理解为一种工具,一种很大的方法工具箱里面的工具。不过,它目前仍然还有很多问题类型以待解决,还需要点其他方法和更多的时间。

Summary

USE方法可以概括为:检查所有的资源的利用率,饱和度,和错误信息。

我们期望大家能尽早使用USE方法去做性能检查,或者是用它确定系统的瓶颈。

名词定义:

  • 资源: 服务器功能性的物理组成硬件(CPU, 硬盘, 总线 )
  • 利用率: 资源执行某工作的平均时间
  • 饱和: 衡量资源超载工作的程度,往往会被塞入队列
  • 错误: 错误事件的数量

分析软件资源,或者是软件的强制性限制(资源控制)也是很有用的,同时要关注哪些指标是处于正常的可接受范围之内的。这些指标通常用以下术语表示:

  • 利用率: 以一个时间段内的百分比来表示,例如:一个硬盘以90%的利用率运行
  • 饱和度: 一个队列的长度,例如:CPUs平均的运行时队列长度是4
  • 错误(数): 可度量的数量,例如:这个网络接口有50次(超时?)

我们应该要调查那些错误,因为它们会降低系统的性能,并且当故障模型处于可回复模式的时候,它可能不会立刻被发现。

这包括了那些失败和重试等操作,以及那些来自无效设备池的失效设备。

低利用率是否意味着未饱和?

即使在很长一段时间内利用率很低,一个爆发增长的高利用率,也会导致饱和 and 性能问题,这点要理解起来可能有违三观!

我举个例子,有一位客户遇到的问题,即使他们的监控工具显示 CPU使用率从来没有超出过 80%,但是 CPU饱和度依然有问题(延迟)监控工具报告了 5分钟的平均值,而其中,CPU利用率曾在数秒内高达 100% 。

资源列表

下面来看如何开始使用。

准备工作时, 你需要一个资源列表来按步就班的去做。 下面是一个服务器的通用列表:

  • CPUs: sockets, cores, hardware threads (virtual CPUs)
  • 内存: 容量
  • 网络接口
  • 存储设备: I/O, 容量
  • 控制器: 存储, 网卡
  • 通道: CPUs, memory, I/O

有些组件分两种类型的资源:存储设备是服务请求资源(I / O)以及容量资源(population), 两种类型都可能成为系统瓶颈。 请求资源可以定义为队列系统,可以将请求先存入排队然后再消化请求。

有些物理组件已被省略,例如硬件缓存(例如,MMU TLB / TSB,CPU)。

USE方法对于在高利用率或高饱和度下,遭受性能退化、导致瓶颈的资源最有效,在高利用率下缓存可以提高性能。

在使用USE方法排除系统的瓶颈问题之后 ,你可以检查缓存利用率和其他的性能属性。

如果你不确认要不要监控某一个资源时,不要犹豫,监控它,然后你就能看到那些指标工作的有多么的棒。

功能模块示意图

另外一种迭代资源的方法,是找到或者绘制一张系统的功能模块示意图。

这些显示了模块关系的图,在你查找数据流的瓶颈的时候是非常有用的,这里有一张Sun Fire V480 Guide (page 82)的例图:

我喜欢这些图表,尽管制作出它很难。 不过,由硬件工程师来画这张图是最适合的-他们最善于做这类事。如果不信的话你可以自己试试。

在确定各种总线的利用率的同时,为每个总线的功能图表,注释好它的最大带宽。这样我们就能在进行单次测量之前,得到能将系统瓶颈识别出来的图表。

Interconnects

CPU,内存和I / O interconnects 往往被忽略。 幸运的是,它们并不会频繁地成为系统的瓶颈。 不幸的是,如果它们真的频繁的成为瓶颈,我们能做的很少(也许你可以升级主板,或减少load:例如,“zero copy”项目减轻内存总线load)。

使用USE方法,至少你会意识到你没有考虑过的内容:interconnect性能。 有关使用USE方法确定的互连问题的示例,请参阅分析Analyzing the HyperTransport。

Metrics

给定资源列表,识别指标类型:利用率,饱和度和错误指标。这里有几个示例。看下面的table,思考下每个资源和指标类型,metric列是一些通用的Unix/Linux的术语提示(你可以描述的更具体些):

resource type metric
CPU utilization CPU utilization (either per-CPU or a system-wide average)
CPU saturation run-queue length or scheduler latency(aka
Memory capacity utilization available free memory (system-wide)
Memory capacity saturation anonymous paging or thread swapping (maybe “page scanning” too)
Network interface utilization RX/TX throughput / max bandwidth
Storage device I/O utilization device busy percent
Storage device I/O saturation wait queue length
Storage device I/O errors device errors (“soft”, “hard”, …)

这些指标是每段间隔或者计数的平均值,作为你的自定义清单,要包括使用的监控软件,以及要查看的统计信息。如果是不可用的指标,可以打个问号。最后,你会完成一个完事的、简单、易读的 metrics 清单.

Harder Metrics

再来看几个硬件指标的组合

resource type metric
CPU errors eg, correctable CPU cache ECC events or faulted CPUs (if the OS+HW supports that)
Memory capacity errors
Network saturation
Storage controller utilization
CPU interconnect utilization
Memory interconnect saturation
I/O interconnect utilization

这些依赖于操作系统的指标一般会更难测量些, 而我通常要用自己写的软件去收集这些指标。

重复所有的组合,并附上获取每个指标的说明,你会完成一个大概有30项指标的列表,其中有些是不能被测量的,还有些是难以测量的。

幸运的是,最常见的问题往往是简单的(例如,CPU饱和度,内存容量饱和度,网络接口利用率,磁盘利用率),这类问题往往第一时间就能被检查出来。

本文的顶部,pic-1中的 example checklists 可作为参考。

In Practice

读取系统的所有组合指标,是非常耗时的,特别是当你开始使用总线和interconnect 指标的情况下。

现在我们可以稍微解放下了,USE方法可以让你了解你没有检查的部分,你可以只有关注其中几项的时间例如:CPUs, 内存容量, 存储容易, 存储设备 I/O, 网络接口等。通过USE方法,那些以前未知的未知指标现在变成了已知的未知指标(我理解为,以前我们不知道有哪些指标会有什么样的数据,现在起码能知道我们应该要关注哪些指标)。

如果将来定位一个性能问题的根本原因,对你的公司至关重要的时候,你至少已经有一个明确的、经过验证的列表,来辅助你进行更彻底的分析,请完成适合你自己的USE方法,有备无患。

希望随着时间的推移,易于检查的指标能得以增长,因为被添加到系统的metrics 越多,会使USE方法将更容易(发挥它的力量)。 性能监视软件也可以帮上忙,添加USE方法向导to do the work for you(do what work? ) 。

Software Resources

有些软件资源可以用类似的方式去分析。 这通常适用于软件的较小组件,而不是整个应用程序。 例如:

  • 互斥锁(mutex locks): 利用率可以定义为锁等待耗时;饱和率定义为等待这把锁的线程个数。
  • 线程池: 利用率可以定义为线程工作的时长;饱和率是等待线程池分配的请求数量。
  • 进程/线程 容量: 系统是有进程或线程的上限的,它的实际使用情况被定义为利用率;等待数量定义为饱和度;错误即是(资源)分配失败的情况(比如无法fork)。(译注:fork是一个现有进程,通过调用fork函数创建一个新进程的过程)
  • 文件描述符容量(file descriptor capacity): 和上述类似,但是把资源替换成文件描述符。

如果这几个指标很管用就一直用,要不然软件问题会被遗留给其他方法了(例如,延迟,后文会提到其他方法:other methodologies )。

Suggested Interpretations

USE方法帮助你定位要使用哪些指标。 在学习了如何从操作系统中读取到这些指标后,你的下一步工作就是诠释它们的值。对于有的人来说, 这些诠释可能是很清晰的(因为他们可能很早就学习过,或者是做过笔记)。而其他并不那么明了的人,可能取决于系统负载的要求或期望 。

下面是一些解释指标类型的通用建议:

  • Utilization: 利用率通常象征瓶颈(检查饱和度可以进一步确认)。高利用率可能开始导致若干问题:
  • 对利用率进行长期观察时(几秒或几分钟),通常来说 70% 的利用率会掩盖掉瞬时的 100% 利用率。
  • 某些系统资源,比如硬盘,就算是高优先级请求来了,也不会在操作进行中被中断。当他们的利用率到 70% 时候,队列系统中的等待已经非常频繁和明显。而 CPU 则不一样,它能在大部分情况下被中断。
  • Saturation: 任何非 0 的饱和度都可能是问题。它们通常是队列中排队的时间或排队的长度。
  • Errors: 只要有一条错误,就值得去检查,特别是当错误持续发生从而导致性能降低时候。

要说明负面情况很容易:利用率低,不饱和,没有错误。 这比听起来更有用 - 缩小调查范围可以快速定位问题区域。

Cloud Computing

在云计算环境中,软件资源控制可能是为了限制 使用共享计算服务的tenants 的流量 。在Joyent公司,我们主要使用操作系统虚拟化(SmartOS),它强加了内存限制,CPU限制和存储I / O限制。 所有这些资源限制,都可以使用USE Method进行检查,类似于检查物理资源。

例如,在我们的环境中,”内存容量利用率”可以是 tenants 的内存使用率 vs 它的内存上限 。即使传统的Unix页面扫描程序可能处于空闲状态,也可以通过匿名页面活动看到”内存容量饱和度”。

Strategy

下面是用流程图 的方式画了USE方法的示意图。 请注意,错误检查优先于利用率和饱和度检查(因为通常错误更快的表现出来,并更容易解释)。

USE方法定位到的问题, 可能是系统瓶颈。 不幸的是,系统可能会遇到多个性能问题,因此您发现的第一个可能的问题最终却不是个问题。 发现的每个问题都可以用方法持续的挖掘,然后继续使用 USE 方法对更多资源进行反复排查。

进一步分析的策略包括工作量特征和 drill-down 分析。 完成这些后,你应该有依据据能判断,纠正措施是要调整应用的负载或调整资源本身。

Apollo

(译者注:Apollo 这一段我们可以不太关注,它主要是讲 USE 方法,与阿波罗登月计划相关的系统设计的一些渊源)

我之前有提到过,USE方法可以被应用到除服务器之外。为了找到一个有趣的例子, 我想到了一个我没有完全不了解的系统,并且不知道从哪里开始:阿波罗月球模块指导系统。USE 方法提供了一个简单的流程来尝试第一步是寻找一个资源列表,或者更理想的话,找到一个功能模块图表。我在 【Lunar Module - LM10 Through LM14 Familiarization Manual】中发现了以下内容:

这些组件中的一部分可能未表现出利用率或饱和度特性。在迭代后,就可以重新绘制只包含相关组件的图表(还可以包括:“可擦除存储”部分的内存,”核心区域 “ 和 “ vac区域 “ 寄存器)。

我将从阿波罗主脑(AGC)本身开始。 对于每个指标,我浏览了各种LM文档,看看哪些是合理的(有意义的):

  • AGC utilization: This could be defined as the number of CPU cycles doing jobs (not the “DUMMY JOB”) divided by the clock rate (2.048 MHz). This metric appears to have been well understood at the time.
  • AGC saturation: This could be defined as the number of jobs in the “core set area”, which are seven sets of registers to store program state. These allow a job to be suspended (by the “EXECUTIVE” program - what we’d call
    a “kernel” these days) if an interrupt for a higher priority job arrives. Once exhausted, this moves from a saturation state to an error state, and the AGC reports a 1202 “EXECUTIVE OVERFLOW-NO CORE SETS” alarm.
  • AGC errors: Many alarms are defined. Apart from 1202, there is also a 1203 alarm “WAITLIST OVERFLOW-TOO MANY TASKS”, which is a performance issue of a different type: too many timed tasks are being processed before returning
    to normal job scheduling. As with 1202, it could be useful to define a saturation metric that was the length of the WAITLIST, so that saturation can be measured before the overflow and error occurs.

其中的一些细节,可能对于太空爱好者来说是非常熟悉的:在阿波罗11号降落的时候发生的著名的 1201(”NO VAC AREAS”)和1202警报。(”VAC”是向量加速器的缩写,用于处理vector quantities作业的额外存储; 我觉得wikipadia上将 “向量”描述为”空”可能是错误的)。

鉴于阿波罗11号的1201警报,可以继续使用其他方法分析,如工作负载表征。 工作负载很多可以在功能图中看到,大多数工作负载是通过中断来生效的。 包括用于跟踪命令模块的会合雷达,即使LM正在下降,该模块也仍然在执行中断AGC(阿波罗主脑)的任务。 这是发现非必要工作的一个例子(或低优先级的工作; 雷达的一些更新可能是可取的,因此 LM AGC可以立即计算出中止路径)。

作为一个更深的例子,我将把会合雷达当作资源去检查. 错误最容易识别。 有三种信号类型: “DATA NO GOOD”, “NO TRACK”, and “SHAFT- AND TRUNNION-AXIS ERROR”。

在有某一小段时间里,我不知道能从哪里开始使用这个方法, 去寻找和研究具体的指标。

Other Methodologies

虽然USE方法可能会发现80%的服务器问题,但基于延迟的方法(例如Method R)可以找到所有的问题。 不过,如果你不熟悉软件内部结构,Method R 就有可能需要花费更多时间。 它们可能更适合已经熟悉它的数据库管理员或应用程序开发人员。

而USE方法的职责和专长包括操作系统(OS)和硬件,它更适合初级或高级系统管理员,当需要快速检查系统健康时,也可以由其他人员使用。

Tools Method

以下介绍一个基于工具的方法流程(我称它作”工具方法”),与USE方法作比较:

  1. 列出可用的性能工具(可以选择性安装或购买其他的)。
  1. 列出每个工具提供的有用的指标
  1. 列出每个工具可能的解释规则

按照这个方法做完后,将得到一个符合标准的清单,它告诉我们要运行的工具,要关注的指标以及如何解释它们。 虽然这相当有效,但有一个问题,它完全依赖于可用(或已知的)的,可以提供系统的不完整视图的工具。 用户也不知道他们得到的是一张不完整的视图 - 所以问题将仍然存在。

而如果使用USE方法,不同的是,USE方法将通过迭代系统资源的方式,来创建一个完整的待确认问题列表,然后搜索工具来回答这些问题。这样构建了一张更完整的视图,未知的部分被记录下来,它们的存在被感知(这一句我理解成前文中提到的:未知 的未知变为已知的未知)。 基于USE,同样可以开发一个清单类似于工具方法(Tool-Method),显示要运行的工具(可用的位置),要关注的指标以及如何解释它。

另一个问题是,工具方法在遍历大量的工具时,将会使寻找瓶颈的任务性能得到分散。而USE方法提供了一种策略,即使是超多的可用工具和指标,也能有效地查找瓶颈和错误。

Conclusion

USE方法是一个简单的,能执行完整的系统健康检查的策略,它可以识别常见的系统瓶颈和错误。它可以在调查的早期部署并快速定位问题范围,如果需要的话,还可以进一步通过其他方法进行更详细的研究。

我在这个篇幅上,解释了USE方法并且提供了通用的指标案例,请参阅左侧导航面板中对应操作系统的示例清单,其建议了应用USE方法的工具和指标。另请参阅基于线程的补充方法,TSA Method。

Acknowledgments

  • 感谢Cary Millsap and Jeff Holt (2003) 在”优化Oracle性能”一文中提到的 Method R方法 (以及其他方法), 使我有了灵感,我应该要把这个方法论写出来。
  • 感谢Sun Microsystems的组织,包括PAE和ISV, 他们将USE方法(那时还没命名)应用于他们的存储设备系列,绘制了标注指标和总线速度的ASCII功能块图表 - 这些都比您想象的要困难(我们应该早些时候询问硬件团队的帮助)。
  • 感谢我的学生们,多年前我授予他们这个方法论,谢谢他们提供给我的使用反馈。
  • 感谢Virtual AGC项目组(The Virtual AGC project),读他们的站点 ibiblio.org 上的文档库,就象是一种娱乐. 尤其是LMA790-2 “Lunar Module LM-10 Through LM-14 Vehicle Familiarization Manual” (48页有功能模块图表), 以及 “阿波罗指导和月球导航模块入门学习指南”, 都很好的解释了执行程序和它的流程图
    (These docs are 109 and 9 Mbytes in size.)
  • 感谢Deirdré Straughan 编辑和提供反馈,这提高了我的认知。
  • 文章顶部的图片,是来自于波音707手册,1969出版。它不是完整的,点击查看完整的版本(译注:为方便阅读,就是下面这张:)

Updates

USE Method updates:(略)

  • It was published in ACMQ as Thinking Methodically about Performance (2012).
  • It was also published in Communications of the ACM as Thinking Methodically about Performance (2013).
  • I presented it in the FISL13 talk The USE Method (2012).
  • I spoke about it at Oaktable World 2012: video, PDF.
  • I included it in the USENIX LISA `12 talk Performance Analysis Methodology.
  • It is covered in my book on Systems Performance, published by Prentice Hall (2013).

More updates (Apr 2014):

  • LuceraHQ are implementing USE Method metrics on SmartOS for performance monitoring of their high performance financial cloud.
  • LuceraHQ正在SmartOS上,为他们高性能金融云的性能监测,实施USE 方法指标
  • I spoke about the USE Method for OS X at MacIT 2014 (slides)。

技术沙龙推荐

点击下方图片即可阅读

从ELK到EFK

最终版 | 深度学习之概述(Overview)

翻译 | 关键CSS和Webpack: 减少阻塞渲染的CSS的自动化解决方案

推荐系统那些事儿

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…395396397…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%