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

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


  • 首页

  • 归档

  • 搜索

Mongoose实战开发-基础篇

发表于 2017-11-07

何为MongoDB?MongoDB 是一个开源的文档数据库,可提供高性能,高可用性和自动缩放。
何为Mongoose?简单的说,Mongoose就是对node环境中MongoDB数据库操作的封装,一个对象模型工具,将数据库中的数据转换为JavaScript对象以供我们在应用中使用。

官网:mongoosejs.com/window下安装MongoDB: www.runoob.com/mongodb/mon…Linux下安装MongoDB:
http://www.runoob.com/mongodb/mongodb-linux-install.html

1、Schema、Model、Entity
在使用Mongoose前,先了解一下 Mongoose 中的三个概念:Schema、Model、Entity

(1) Schema
Schema是一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力。其实也可以看作是表结构的定义。

如何创建一个Schema?

1
2
3
4
5
6
7
复制代码const Articles = new Schema({   
  articleId: {type: String},   
  title: { type: String },   
  content: { type: String },   
  by: { type: String},   
  modifyOn: { type: Date, default: Date.now },  
}, { collection: 'articles' });

(2) Model

由Schema发布生成的模型,具有抽象属性和行为的数据库操作对象。正是Model的存在,让我们操作数据库更加方便快捷。

依赖Schema生成一个Model:

1
复制代码mongoose.model('articles', Articles);

(3) Entity

由Model创建的实体,它的操作也会影响数据库。

依赖Model,创造一个Entity:

1
复制代码const article = new ArticlesModel(req.body);  // 相当于调用了Model.create(req.body)

2、文章管理系统
理解了Schema、Model、Entity后,接下来我们来开始创建一个简单的文章管理系统。
技术栈:Mongoose:一个对象模型工具Express:基于Node.js 平台,快速、开放、极简的 web 开发框架。Pug:基于Node.js的模板引擎
Lodash:一个具有一致接口、模块化、高性能等特性的 JavaScript 工具库。

完整项目:Github:mongodb-pratice
启动项目:

1
2
3
4
5
6
7
复制代码cd mongoose-demo   


npm install   


npm start

项目结构说明:

  • db:数据库相关操作文件,比如连接connect.js
  • modules:模型,比如artilces,里面包含了路由、方法、模型等
  • views:模板文件
  • public:静态资源
  • main.js:启动文件

3、项目详解
(1) 连接数据库
Mongoose连接数据库很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码// db/connect.js  
const mongoose = require('mongoose');   


exports.start = (success) => {   
  mongoose.connect('mongodb://localhost/blog', { useMongoClient: true });   
  const db = mongoose.connection;   
  db.on('error', console.error.bind(console, 'connection error:'));   
  db.once('open', () => {   
    // we're connected!   
    console.log('connected db: blog');   
    if (success) {   
      success();   
    }   
  });  
};

上面的代码表示连接本地数据库blog,同时绑定事件监听器来监听数据库是否连接成功。

然后在启动文件中调用:

1
2
3
复制代码// main.js  
const db = require('./db/connect');   
db.start();

当然,你也可以将两个代码放在同一个文件。

如果需要用户和密码,可以这样:

1
复制代码mongoose.connect('mongodb://root:password@localhost/blog')

当连接数据库成功后,我们就可以操作数据库了。

(2) 创建模型

这里我们基于一个简单的文章系统来建模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// modules/articles/articles.model.js   
const mongoose = require('mongoose'),   
Schema = mongoose.Schema;  


//先创建Schema  
const Articles = new Schema({   
  articleId: {type: String},   
  title: { type: String },   
  content: { type: String },   
  by: { type: String},   
  modifyOn: { type: Date, default: Date.now },  
}, { collection: 'articles' });



//通过Schema创建Model  
mongoose.model('articles', Articles);

第一个参数就是结构对象,每一个键就是一个字段,你可以定义类型/默认值/验证/索引等;第二个参数是可选的(默认是取model的第一个参数加s),用来自定义Collection的名称。

当然,你还可以这样定义名称:

1
复制代码mongoose.model('articles', Articles, 'articles');

注:我们不需手动去创建Collection,当你操作时,如Collection不存在,会自动创建。

(3) 配置API接口

在上面的代码中,我们连接了数据库,同时创建了可操作的Model,接下来我们就需要配置API接口供我们调用,这里就需要借助Express了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码// modules/articles/articles/route.js  
const articleControllder = require('./articles.controller');   


module.exports = (app) => {   
  app.route('/api/article/add')   
    .post(articleControllder.add);       


  app.route('/api/article/find')   
    .get(articleControllder.get);    


  app.route('/api/article/remove')   
    .delete(articleControllder.remove);    


  app.route('/api/article/update')   
    .put(articleControllder.update);  
};

上面的代码中提供了增删改查四个api。

(4) 基础操作(CRUD)

在上面路由中调用的方法:

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
复制代码// modules/articles/articles.controller.js  
const mongoose = require('mongoose');  
const ArticlesModel = mongoose.model('articles');   
mongoose.Promise = global.Promise;  
const commonFunction = require('../common/common_function');  
const _ = require('lodash');   


exports.get = (req, res) => {   
  const articleId = req.query['id'];   
  ArticlesModel.findById(articleId, (err, result) => {   
    if (err) {   
      return res.status(400).send({   
                message: '查找失败',   
                data: []   
             });   
    } else {   
      res.jsonp({   
        data: [result]   
      });   
    }   
  });  
};   


exports.add = (req, res) => {   
  req.body['articleId'] = commonFunction.getRandom();   
  req.body['modifyOn'] = new Date();   
  req.body['by'] = 'TG';   
  const article = new ArticlesModel(req.body);   
  article.save((err) => {   
    if (err) {   
      return res.status(400).send({   
             message: '新增失败',   
             data: []   
           });   
    } else {   
      res.jsonp({   
        data: [article]   
      });   
    }   
  }); 
};   


exports.remove = (req, res) => {   
  const id = req.query['id'];   
  ArticlesModel.remove({'_id': id}, (err) => {   
    if (err) {   
       return res.status(400).send({   
            message: '删除失败',   
            data: []   
        });   
    } else {   
      res.jsonp({status: 1});   
    }   
  })  
};   


exports.update = (req, res) => {   
  const id = req.body['id'];   
  ArticlesModel.findById(id, (err, result) => {   
    if (err) {   
       return res.status(400).send({   
            message: '更新失败',   
            data: []   
       });   
    } else {   
       delete req.body['id'];   
       const articles = _.extend(result, req.body);   
       articles.save((err, result) => {   
          if (err) {   
             return res.status(400).send({   
                  message: '更新失败',   
                  data: []   
              });   
          } else {   
             res.jsonp({ data: [articles] });   
          }   
       })   
    }   
  })  
};

这就是最基础的CRUD操作。

可能你会疑惑我没有导出Model,为什么这里可以用,其实我在引导文件中已经默认加载所有已发布的Model。

1
2
3
4
5
6
7
8
9
10
11
复制代码// main.js  
...  
const loadModels = () => {   
  files.forEach((v, k) => {   
    if (/model.js$/.test(v)) {   
      require(path.resolve(v));   
    }   
  });  
};  
loadModels();  
...

通过获取modules文件夹里的所有文件,然后匹配后缀为model.js的模型文件,这样,你就可以在任何地方获取模型:

1
复制代码const ArticlesModel = mongoose.model('articles');

你还可以通过这篇文章了解MongoDB常用语句: 《MongoDB常用语句》

这里只是简单的讲解了Mongoose最基本的连接和CRUD,还有更多的内容需要我们去深入学习研究。

如有任何疑问或意见,欢迎在下方的评论区留言!

本文转载自: 掘金

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

聊一聊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属于同一个递增序列,这样可以为以后的融合,或者平台化减少数据整合的复杂度。
● 数据模型层面,尽量将命令模型和查询模型分开,一方面可以将主流程和数据展示操作彻底分开;一方面也可以降低数据操作的复杂性。
● 系统的流量控制和切换粒度要足够精细,这样可以提高应对风险的能力。

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

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

五、总结和感谢

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

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

本文转载自: 掘金

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

Jmeter接口测试+压力测试

发表于 2017-10-31

  jmeter是apache公司基于java开发的一款开源压力测试工具,体积小,功能全,使用方便,是一个比较轻量级的测试工具,使用起来非常简单。因为jmeter是java开发的,所以运行的时候必须先要安装jdk才可以。jmeter是免安装的,拿到安装包之后直接解压就可以使用,同时它在linux/windows/macos上都可以使用。   jmeter可以做接口测试和压力测试。其中接口测试的简单操作包括做http脚本(发get/post请求、加cookie、加header、加权限认证、上传文件)、做webservice脚本、参数化、断言、关联(正则表达式提取器和处理json-json
path extractor)和jmeter操作数据库等等。

接口测试

Jmeter-http接口脚本

一般分五个步骤:(1)添加线程组 (2)添加http请求 (3)在http请求中写入接入url、路径、请求方式和参数 (4)添加查看结果树 (5)调用接口、查看返回值

  • jmeter 发get请求

这里写图片描述

这里写图片描述

  • jmeter 发post请求

这里写图片描述

  • jmeter 添加cookie

需要在线程组里添加配置元件—HTTP Cookie 管理器

这里写图片描述

这里写图片描述

  • jmeter 添加header
    需要在线程组里面添加配置元件—HTTP信息头管理器
    这里写图片描述

这里写图片描述

  • jmeter 上传文件

这里写图片描述

  • jmeter 参数化 入参经常变化的话,则可以设置成一个变量,方便统一修改管理;如果入参要求随机或可多种选择,则通过函数生成器或者读取文件形成一个变量。所以参数化有三种方式:用户定义的变量、函数生成器、读取文件。

(1)用户定义的变量   需要添加配置元件-用户定义的变量。

这里写图片描述

这里写图片描述

(2)函数生成器   需要用到函数助手功能,可以调用函数生成一些有规则的数据。常用的几个函数有_uuid、_random、_time。_uuid会生成一个随机唯一的id,比如在避免java请求重发造成未处理数据太多的情况,接口请求可加一个唯一的请求id唯一的响应id进行一一对应;随机数_random,可以在你指定的一个范围里取随机值;取当前时间_time,一些时间类的入参可以使用,如{__time(,)} 是生成精确到毫秒的时间戳、{__time(/1000,)}是生成精确到秒的时间戳、${__time(yyyy-MM-dd
HH:mm:ss,)} 是生成精确到秒的当前时间。

这里写图片描述

(3)从文件读取
  需要在线程组里面添加配置元件-CSV Data Set Config
  其中Recycle on EOF:设置True后,允许循环取值
这里写图片描述
  具体的例子如下所示:

这里写图片描述

这里写图片描述

这里写图片描述

  • jmeter 断言
      jmeter断言用来检测响应返回的结果和我们预期的是否一致。若针对整个线程组的话,则在线程组下添加断言-响应断言;若只是针对某个请求的话,则在请求下添加断言-响应断言。

这里写图片描述

这里写图片描述

  • jmeter关联

  接口请求之间存在参数调用,为了保存这个参数,建立jmeter关联。比如登陆接口和购买商品接口,购买商品接口就需要登陆接口返回的token等登陆信息,jmeter关联就可以保存这个token信息,方便购买商品接口使用。
  jmeter关联可以通过二种方式来完成,获取到返回结果中指定的值。它们分别是正则表达式提取器、 json path extractor。
(1)正则表达式提取器
若想获取的返回值未匹配到,可以把正则表达式两边匹配的数据扩大点。

这里写图片描述

a. 关于正则表达式

():括起来的部分就是要提取的。
.:匹配除换行外的任何字符串。
+:代表+号前面的字符必须至少出现一次(一次或多次)。
?:代表?前面的字符最多可以出现一次,在找到第一个匹配项后停止(0次或1次)。
:代表*号前面的字符可以不出现,也可以出现一次或者多次(0次、1次或者多次)
(.\
):贪婪模式,匹配尽可能多的字符
(.*?)或(.+?):匹配尽可能少的字符,一旦匹配到第一个就不往下走了。

b. 关于模板

  若想提取多个值的话,比如是a和b这两个值,则可以写成:$1?2$。无论要提取多少个值,引用名称就是一个的,比如名称为id,${id_go}:获取整个字符串ab,${id_g1}:获取的是a,${id_g2}:获取的是b。
  下面有一个具体的实例,如下图所示:

这里写图片描述

这里写图片描述

这里写图片描述

(2)json path extractor
  jmeter通过安装json path extractor插件来处理json串,提取json串中的字段值。插件的下载地址:jmeter-plugins.org/?search=jpg…,下载完成,解压后,直接把lib文件夹放到jmeter相应目录下面。特别说明:jmeter
2.xx左右的版本尝试过无法使用该插件,在jmeter 3.xx左右的版本装完插件后能正常使用。
  需要在请求下创建后置处理器-jp@gc-JSON Path Extractor,具体的实例如下所示:

这里写图片描述

这里写图片描述

这里写图片描述

  关于json path相关插件的方法和使用,推荐可以看这篇博客: www.jianshu.com/p/56a607fc0…

  • jmeter 操作数据库   操作数据库基本有四个步骤:(1)导入mysql的jdbc的jar包 (2)创建数据库的连接配置,线程组里添加配置元件-JDBC Connection Configuration (3)线程组里添加jdbc request,写sql语句 (4)添加察看结果树,点击启动按钮,就能看到执行的SQL。具体的实例如下截图所示:

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

  特别说明:jmeter还可以操作oracle、postgreSQL、msSQL、mongodb等等数据库,同时不同的数据库,JDBC Connection Configuration填写的Database url格式和JDBC Driver驱动名称也不相同。jmeter数据库驱动列表如下表所示:

数据库 驱动 数据库url
mysql com.mysql.jdbc.Driver jdbc:mysql://host:port/{dbname}?allowMultiQueries=true
oracle org.postgresql.Driver dbc:postgresql:{dbname}

Jmeter-webservice接口脚本

  基本分为五个步骤:(1)先需要通过soapui工具获取到webservice接口的请求地址、请求报文和请求soapaction。(2)jmeter新建一个线程组 (3)线程组下建立SOAP/XML-RPC Request,写入请求url、请求报文、请求soapaction。(3)启动jmeter,调用接口,通过察看结果树查看返回值。   soapui获取信息的实例如下图所示:

这里写图片描述

  soapui提交完后,点击raw,可看到soapation,有些接口若没返回soapation,则jmeter里也就不用填。

这里写图片描述
   jmeter-webservice脚本实例如下图所示:

这里写图片描述

压力测试

  压力测试分两种场景:一种是单场景,压一个接口的;第二种是混合场景,多个有关联的接口。压测时间,一般场景都运行10-15分钟。如果是疲劳测试,可以压一天或一周,根据实际情况来定。

压测任务需求的确认

压测前要明确压测功能和压测指标,一般需要确定的几个问题:

  1. 固定接口参数进行压测还是进行接口参数随机化压测?
  2. 要求支持多少并发数?
  3. TPS(每秒钟处理事务数)目标多少?响应时间要达到多少?
  4. 压服务器名称还是压服务器IP,一般都是压测指定的服务器

压测设置

  1. 线程数:并发数量,能跑多少量。具体说是一次存在多少用户同时访问
  2. Rame-Up Period(in seconds):表示JMeter每隔多少秒发动并发。理解成准备时长:设置虚拟用户数需要多长时间全部启动。如果线程数是20,准备时长为10,那么需要10秒钟启动20个数量,也就是每秒钟启动2个线程。
  3. 循环次数:这个设置不会改变并发数,可以延长并发时间。总请求数=线程数*循环次数
  4. 调度器:设置压测的启动时间、结束时间、持续时间和启动延迟时间。

压测结果查看

  运行完后,聚合报告会显示压测的结果。主要观察Samples、Average、error、Throughput。

  1. Samples:表示一共发出的请求数
  2. Average:平均响应时间,默认情况下是单个Request的平均响应时间(ms)
  3. Error%:测试出现的错误请求数量百分比。若出现错误就要看服务端的日志,配合开发查找定位原因
  4. Throughput:简称tps,吞吐量,默认情况下表示每秒处理的请求数,也就是指服务器处理能力,tps越高说明服务器处理能力越好。

压测结果的分析

  1. 有错误率同开发确认,确定是否允许错误的发生或者错误率允许在多大的范围内;
  2. Throughput吞吐量每秒请求的数大于并发数,则可以慢慢的往上面增加;若在压测的机器性能很好的情况下,出现吞吐量小于并发数,说明并发数不能再增加了,可以慢慢的往下减,找到最佳的并发数;
  3. 压测结束,·登陆相应的web服务器查看CPU等性能指标,进行数据的分析;
  4. 最大的tps:不断的增加并发数,加到tps达到一定值开始出现下降,那么那个值就是最大的tps。
  5. 最大的并发数:最大的并发数和最大的tps是不同的概率,一般不断增加并发数,达到一个值后,服务器出现请求超时,则可认为该值为最大的并发数。
  6. 压测过程出现性能瓶颈,若压力机任务管理器查看到的cpu、网络和cpu都正常,未达到90%以上,则可以说明服务器有问题,压力机没有问题。
  7. 影响性能考虑点包括:数据库、应用程序、中间件(tomact、Nginx)、网络和操作系统等方面。

jmeter在linux下进行压力测试

  1. jmeter 在linux安装
      简单说下,就是要先安装jdk,同时再配置环境变量,最后再上传jmeter压缩的安装包,在linux下解压完安装包就可以使用了。推荐博客:blog.csdn.net/zhemeteor/a…
  2. jmeter在linux运行 进入jmeter下的bin目录下运行脚本,未配置jmeter环境变量的条件下,运行的命令:

./jmeter -n -t a.jmx -l res.jtl

其中a.jmx是准备好的jmeter脚本,res.jtl是测试结果文件,测试结果文件可以导入到jmeter察看结果树下查看。

jmeter添加负载机

本文转载自: 掘金

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

Redis分布式锁的正确实现方式(Java版) 前言 可靠性

发表于 2017-10-31

本博客使用第三方开源组件Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景。

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。


可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

1
2
3
4
5
复制代码<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

加锁代码

正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

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
复制代码public class RedisTool {

private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;

}

}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx()和jedis.expire()组合实现加锁,代码如下:

1
2
3
4
5
6
7
8
9
复制代码public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}

}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

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
复制代码public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}

// 如果锁存在,获取锁的过期时间
String currentValueStr = jedis.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockKey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
return true;
}
}

// 其他情况,一律返回加锁失败
return false;

}

那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

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

private static final Long RELEASE_SUCCESS = 1L;

/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;

}

}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

1
2
3
复制代码public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

1
2
3
4
5
6
7
8
9
复制代码public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {

// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}

}

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。


总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,链接在参考阅读章节已经给出。


参考阅读

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson


本文转载自: 掘金

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

项目开发框架-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等。当前本文的列子就是一个空壳子,以备参考吧】

本文转载自: 掘金

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

腾讯基于PHP的开源服务器框架

发表于 2017-10-27

Overview

Tencent Server Framework is a coroutine and Swoole based server framework for fast server deployment which developed by Tencent engineers.

Features

  • PHP Based. Compared with C++, the framework is more efficient in developing and programing.
  • based on Swoole extension. powerful async IO, timers and other infrastructure capacity can be used in this framework.
  • support PHP coroutine. Synchronous programing is possible using the coroutine schedule system, and can lead to the similar server capability with that of server deveoped in an asynchronous way.
  • support server monitor and provide interface to add more rules

Requirements

  • php5.5+
  • Swoole1.7.18+
  • linux,OS X

Installation

  • PHP install
  • Swoole extension install

Introduction

  • Tencent Server Framework can help you to start your server quickly,you just need to set a few settings

Server

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码vim server.ini

[server]
;server type:tcp,udp,http
type = http
; port
listen[] = 12312
; entrance file
root = '/data/web_deployment/serv/test/index.php'
;php start path
php = '/usr/local/php/bin/php'

[setting]
; worker process num
worker_num = 16
; task process num
task_worker_num = 0
; dispatch mode
dispatch_mode = 2
; daemonize
daemonize = 1
; system log
log_file = '/data/log/test.log'

How

to start your server

1
2
复制代码cd /root/tsf/bin/
php swoole testHttpServ start
  • Support Cmds: start,stop,reload,restart,status,shutdown,startall,list

How

to use TCP/UDP/HTTP Client

  • we support different network protocols: TCP,UDP,HTTP
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
复制代码
$tcpReturn=(yield $this->tcpTest());

$udpReturn=(yield $this->udpTest());

$httpReturn=(yield $this->httpTest());

public function tcpTest(){
$ip = '127.0.0.1';
$port = '9905';
$data = 'test';
$timeout = 0.5; //second
yield new Swoole\Client\TCP($ip, $port, $data, $timeout);
}

public function udpTest(){
$ip = '127.0.0.1';
$port = '9905';
$data = 'test';
$timeout = 0.5; //second
yield new Swoole\Client\UDP($ip, $port, $data, $timeout);
}

public function httpTest(){
$url='http://www.qq.com';
$httpRequest= new Swoole\Client\HTTP($url);
$data='testdata';
$header = array(
'Content-Length' => 12345,
);
yield $httpRequest->get($url); //yield $httpRequest->post($path, $data, $header);
}

How

to use Muticall

  • Beside that,we also support Muticall:
  • you can use Muticall to send TCP,UDP packets at the sametime
  • when all the requests come back,return to interrupt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码  
$res = (yield $this->muticallTest());

public function muticallTest(){
$calls=new Swoole\Client\Multi();
$firstReq=new Swoole\Client\TCP($ip, $port, $data, $timeout);
$secondReq=new Swoole\Client\UDP($ip, $port, $data, $timeout);
$thirdReq= new Swoole\Client\HTTP("http://www.qq.com");

$calls ->request($firstReq,'first'); //first request
$calls ->request($secondReq,'second'); //second request
$calls ->request($thirdReq,'third'); //third request
yield $calls;
}

var_dump($res)

Concect

to mysql async

1
2
3
4
5
6
复制代码
$sql = new Swoole\Client\MYSQL(array('host' => '127.0.0.1', 'port' => 3345, 'user' => 'root', 'password' => 'root', 'database' => 'test', 'charset' => 'utf-8',));
$ret = (yield $sql ->query('show tables'));
var_dump($ret);
$ret = (yield $sql ->query('desc test'));
var_dump($ret);

Router

  • We support individuation route rules
  • now we realize some universal route rules and restful rules
  • besides that, we also support default GET parameter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码  URL                                       METHOD       CONTROLLER  ACTION
http://127.0.0.1:80/Test?h=1 ANY ==> TestController/ActionIndex

http://127.0.0.1:80/Test/send?h=1 ANY ==> TestController/ActionSend
Restful
http://127.0.0.1:80/rest GET ==> TestController/ActionList
http://127.0.0.1:80/rest/Test/22 GET ==> TestController/ActionView
Get['id']=22
http://127.0.0.1:80/rest/Test POST ==> TestController/ActionCreate
http://127.0.0.1:80/rest/Test/22 PUT ==> TestController/ActionUpdate
Get['id']=22
http://127.0.0.1:80/rest/Test/22 DELETE ==> TestController/ActionDelete
Get['id']=22
http://127.0.0.1:80/rest/Test/send/1/li GET ==> TestController/ActionSend
Get['cid']=1 Get['name']=li

Performance

Contribution

Your contribution to TSF development is very welcome!

You may contribute in the following ways:

  • Repost issues and feedback
  • Submit fixes, features via Pull Request
  • Write/polish documentation

本文转载自: 掘金

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

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

本文转载自: 掘金

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

利用Apache工具和Guava对ArrayList进行分页

发表于 2017-10-25

概述


之前写过一篇对ArrayList进行分页,介绍如何用JAVA API对ArrayList进行分页,下面再介绍另外两种方法。


使用Apache工具


使用Apache的ListUtils类,一行代码即可解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码import java.util.Arrays;
import java.util.List;

import org.apache.commons.collections4.ListUtils;

import com.alibaba.fastjson.JSON;

public class TestArrayListPartion {

public static void main(String[] args) {
List<String> list = Arrays.asList("1","2","3","4","5","6","7");
List<List<String>> partition = ListUtils.partition(list, 2);
System.out.println(JSON.toJSONString(partition));
}
}

使用Guava工具


使用Guava的Lists类,一行代码即可解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码import java.util.Arrays;
import java.util.List;


import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;

public class TestArrayListPartionUseGuava {

public static void main(String[] args) {
List<String> list = Arrays.asList("1","2","3","4","5","6","7");
List<List<String>> partition = Lists.partition(list, 2);
System.out.println(JSON.toJSONString(partition));
}
}

本文转载自: 掘金

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

二叉树在实际项目中的应用 二叉树模型在实际项目中的应用

发表于 2017-10-23

二叉树模型在实际项目中的应用

【云计算尖子班】 技术分享

分享时间:2017.04.14

分享人:海诺

主题:《二叉树模型在实际项目中的应用》

[TOC]

首先向大家道歉,没有在云计算方面分享内容。我的内容是来源于最近几次的外包项目中,单产品金字塔体系的研究,希望能给大家一定的收获。

概述

金子塔是销售行业对二叉树的一个称呼,可能还有别的例如正三角,或者大三角小三角之说但总归离不开二叉树这个模型。

金字塔为什么是二叉树模型

  1. 首先金字塔的每一个节点需要一个上级,最顶级不需要
  2. 金字塔的每一个节点都可以且最多只能放置两个下级
  3. 整个金字塔内部节点,随着层数的增加呈现出以2的n次方方式的顺序

第一层 $2^0$

第二层 $2^1$
….
第n层 $2^{n-1}$

二叉树的缺点和优点

  1. 优点,可计算,每一个节点,每一个节点的数量都可以进行计算,而且可以很快的进行定位,即便是每一层不满员
  2. 缺点,缺点是每一节点下面有且最多只有2个下级节点。
    二叉树的计算模型

A

二叉树算法模型

  1. 四角标号 每一个节点有一个定义 定义四个角
1
2
3
4
复制代码左上角表示层级 
右上角表示邀请人
左下角表示左编号
右下角表示右编号

一般通用定义只定义两个就是左编码和右编码

  1. 层级规格 那一层那么他的层号为n-1
  2. 邀请人是根据实际情况来的,就是A邀请F那么F的右上角标记为A
  3. 左编码和右编码 除了第一层的左编码永远是1之外其他的左编码和右编码均会在节点增加时进行加法操作,每增加一个节点 节点后面的右编码和左编码都会进行更改做+2操作

二叉树算法模型示例图

完整的算法图形
A 2

二叉树的算法 节点左右编码算法

  1. 节点左上角的layer值算法就是$n-1$ 第一层就是$1-1 = 0$ 以此类推
  2. 左右下角的编码计算,当仅有一个节点A时,表示最顶级,左编码为1,右编码为2,增加一个人那么就会默认放在这个节点的左下节点,再增加一个为右下节点。先说增加左下节点的时候,新增加的左下节点为父级的左编码+1所以A的左下节点B的左编码为2,这时候B在最下方,B的右编码为左编码+1=3,A的右编码为B的右编码+1=4,当增加A的右下节点C时(这时候一定是存在左下节点的)那么C的左编码为左侧节点的右编码+1=4,C为最下方,C的右编码为自身左编码+1=5,A的右编码为C的右编码+1……以此类推,我们可以看到每增加一个节点A的右编码都会增加2,增加节点后左右编码值大于等于增加节点左编码值的所有值都+2;
  3. 层数计算 layer在实际当时是不可能通过1的方法得到的,因为新增的一个节点,电脑不会得到他是那一层。我们可以通过这样计算层,有两种方法:
    1. 计算他与最顶级之间的节点数 这个可以看西面的具体算法
    2. 读取他父节点的layer,然后+1
  4. 那么我们先看看如何计算总节点数,得到每个节点的信息
    假设我们把这个信息放入到数据库中,定义这样几个字段user,puser,layer,left,right 分别对应当前人,邀请人,层级,左编码,右编码。
    那么我们可以通过'select user from table where left >=1 and right <= 12[A对应的right值] 来得到所有节点,当然也可以通过select * from table得到所有节点,查询出数据后我们可以count()一下得到数组的长度,然后就得到所有节点和总节点数
    计算节点数,还有两种方法,
    1. 如果我们已经知道A的左右值为1和12,那么我们可以很轻松的使用12/2=6得到数据结果。
    2. 如果我们得到最下级节点信息,可以通过 $2^{layer+1}-1$ 来得到所有的节点数目
  5. 如何计算某条分支节点数,每个节点的信息
    仍然是遵循4的假设,按照图形示例中完整的二叉树算法图的信息来计算。我们如何获取到B 和F分支有几个节点以及节点信息呢?
    1. 引子:计算B下的节点数可以通过,【B的右编码】5-【B的左编码】2 然后减去【自身】1=1 来得到。示例中的图形是只有一个节点,但是节点多了呢依然如此,你可以自行计算下C下面的节点数。
    2. 有了上面的引子,我们来看如何获取节点信息。select * from table where left>=1 and left <=3 and right >=4 and right<=12 我们就可以轻松的查询出节点的信息,
    3. 当然我们也可以通过layer的减法来得到节点数例如 【F的layer】2-【A的layer】0 +1 =3,但是该方法无法得到节点信息。
  6. 如何计算A总共的邀请人数
    邀请人数我们一般会放在另一个表中单独处理,但是在当前的4的假设中,我们可以统计有效的邀请人数。 select * from table where puser='A' and left >1 and left <12; 只需要得到邀请人的信息即可。(这里有一个隐藏的原理,属于分销体系,你邀请的人只能在你的下方节点中)
    更简单的方法 select * from table where puser='A'
  7. 计算是否满员,有些销售体系在使用邀请制后,会制定特殊规则比如用户小金字塔必须三级。也就是说需要达到$2^0+2^1+2^2+2^3=2^{3+1}-1=15$ 人。这里怎么算呢可以通过layer来计算,以A为例子,select count(user) as num from table where layer between 0 and 3 and left >=1 and right <=12 即可得到。

总结

以上的内容是在实际开发中经常用到销售系统二叉树模型,感谢大家和我共同学习这个算法,限于个人水平和研究深度,如有不足和错误,恳请大家指正和批评。


海诺@云计算尖子班

本文转载自: 掘金

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

1…950951952…956

开发者博客

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