leveldb文档翻译(1)-leveldb概览

这是leveldb源代码包中提供的 readme.md,index.md 的部分翻译文档,不是逐字逐句翻译,只是把自己感兴趣的部分翻译出来了,作为看代码之前的准备工作。

1.公共接口

include文件夹下面的是公共接口,所有使用者应该从这里的头文件直接调用。

  • include/db.h:数据库的主接口:从这里开始
  • include/options.h:控制整个数据的行为,也控制数据库的读写,可以看做是配置文件。
  • include/comparator.h:用户自定义的比较函数的抽象。如果想要使用基于字节的方法对键大小进行比较,可以使用默认的比较器,用户也可以自己实现比较器来按自己的想法对存储进行排序。
  • include/iterator.h:遍历数据的接口。可以从DB对象中获取这样一个迭代器。
  • include/write_batch.h:对数据库原子的进行多个更新操作的接口。
  • include/slice.h:一个简单的模块,用来维护一个指针和长度,来表示一个字节数组。
  • include/status.h:用来汇报成功或是其他各种各样的错误。
  • include/env.h:操作系统环境的抽象。这个接口的posix实现是在util/env_posix.cc中
  • include/table.h,include/table_builder.h:低层次的模块,绝大多数客户端不会使用这个接口。

2.leveldb基本使用

leveldb提供持久化的键值存储。键和值是任意的字节数组。键值对的存储是按照键的顺序存储的,并且这个顺序是按照用户自定义的排序函数来的。

2.1 打开一个数组库

leveldb存储是使用文件系统中的文件夹。所有数据库内容都会存储在这个文件夹里。下面是一个打开数据库的例子,并且如果数据库不存在会自动创建:

#include <cassert>
#include "leveldb/db.h"

leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
assert(status.ok());

如果希望在数据库已经存在时报错,在leveldb::DB::Open前调用:

options.error_if_exists = true;

2.2 状态

上面的代码中使用了leveldb::Status类型,leveldb中大多数函数都会返回这个值。你可以检查这个值是不是ok,或者打印出相关的错误。

leveldb::Status s = ....;
if (!s.ok()) cerr << s.ToString() << endl;

2.3 关闭数据库

如果数据库操作结束后,删除数据库对象即可关闭

.... open the db as described above ...
.... do something with db ....
delete db;

2.4 读写

数据库提供了Put、Delete、Get方法来修改/查询数据库。例如,下面的代码用来将key1的值移动到key2中。

std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value);
if (s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);

2.5 原子更新

上面的例子中,如果在Put完key2的值,Delete key1的值前,进程崩溃了,同一个值就会被存储在多个key下。使用WriteBatch类可以原子的执行多个更新操作:

#include "leveldb/write_batch.h"
....
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) {
  leveldb::WriteBatch batch;
  batch.Delete(key1);
  batch.Put(key2, value);
  s = db->Write(leveldb::WriteOptions(), &batch);
}

WriteBatch存储了一系列的对数据库的编辑操作,这些操作会被顺序执行。注意到我们先执行了Delete,再执行Put,这样当key1和key2相同时,我们不会错误的删除所有的值。
WirteBatch不仅仅有原子操作的特点,还可以用来加速大量的更新操作。原理是将大量的单独的变化放入同一个批操作中。

2.6 同步写入

默认情况下,对leveldb的写入都是异步的:它在将进程中的写操作提交给操作系统后返回。数据从操作系统内存转移到持久化存储这个过程是异步的(也就是和这里不会等持久化之后才返回)。使用同步写入,可以让某些特定的写入等到数据被写入持久化存储后才返回。(在Posix系统中,这个通过调用 fsync(....)fdatasync(....)msync(...., MS_SYNC) 来实现。

leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ....);

异步写入通常情况下是千百倍的快于同步写入。异步写入的缺点是如果机器崩溃可能会造成最后的一些更新丢失。注意仅仅是写进程的崩溃不会造成任何数据丢失,即使没有使用同步写。因为写进程只负责将写操作提交给操作系统,提交完就意味着写入已经结束(即使这个时候没有真正的写入到持久化存储中)。

异步写大多数时候都能被安全使用。比如,当载入大量数据到数据库中,如果出现崩溃你可以重新开始这个载入操作。也可以使用混合写入模式,也就是每多少个异步写之后使用一次同步写。如果崩溃了,可以从上一次同步写开始(同步写会产生一个标记,所以可以从上一次同步写开始)。

WriteBatch对于异步写提供了多个选择。多个更新操作也被放在同一个批操作中,然后使用一个同步写被一起执行(write_options.sync设置为true)。同步写产生的额外性能消耗被均摊在所有的写操作上。

2.7 并发

数据库在同一时间只能由一个进程打开。leveldb使用了操作系统的锁来防止被错误使用。在一个单进程中,同一个leveldb::DB对象可以被多个并发线程安全的共享。不同的线程之间在对同一个数据库对象写入或者遍历或者调用Get时,不需要额外的同步操作(leveldb的实现会自动做这个必要的同步操作。然而其他对象(比如Iterator和WriteBatch)可能需要额外的同步。如果两个线程共享了这样的对象,它们必须使用它们自己的锁协议来保护对象的使用。更多的细节可以在公共头文件中看到。

2.8 迭代器(Iteration)

下面的例子展示了如何打印一个数据库中所有的键值对。

leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
  cout << it->key().ToString() << ": "  << it->value().ToString() << endl;
}
assert(it->status().ok());  // Check for any errors found during the scan
delete it;

下面的例子展示了如何显示一个特定范围的数据
[start,limit):

for (it->Seek(start);
   it->Valid() && it->key().ToString() < limit;
   it->Next()) {
  ....
}

你也可以倒序遍历所有的数据(倒序比正序可能要慢)

for (it->SeekToLast(); it->Valid(); it->Prev()) {
  ....
}

2.9 快照(Snapshots)

快照提供了键值存储所有状态一致的只读视图。ReadOptions::snapshot可能是non-NULL的值,来表示一个读操作应该执行在特定的DB状态版本上。如果 ReadOptions::snapshot是NULL,读操作可能执行在当前状态的隐式快照上。

快照的创建:DB::GetSnapshot()

leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
.... apply some updates to db ....
leveldb::Iterator* iter = db->NewIterator(options);
.... read using iter to view the state when the snapshot was created ....
delete iter;
db->ReleaseSnapshot(options.snapshot);

当一个快照不需要的时候,使用DB::ReleaseSnapshot接口来释放。

2.10 片(Slice)

*Slice不知道翻译成什么*

it->key()it->value()的返回值都调用了leveldb::Slice类型的实例。Slice是一个简单的结构:包含字节数组的长度和指针。返回一个Slice比返回std::string是一个更轻量划算的选择,因为我们不需要复制潜在的很大的键和值(返回的是指针和长度)。额外的,leveldb的方法不返回null-terminated(\0结尾)的 C-style 字符串,因为leveldb的键和值是允许存储'\0'字节的。

Slice和C++字符串以及C-style字符串之间可以互相转换。

leveldb::Slice s1 = "hello";

std::string str("world");
leveldb::Slice s2 = str;
std::string str = s1.ToString();
assert(str == std::string("hello"));

使用Slice时要小心,因为Slice取决于调用者来保证Slice中存储的数据的生命周期。下面就是一段有问题的代码的示例:

leveldb::Slice slice;
if (....) {
  std::string str = ....;
  slice = str;
}
Use(slice);

当执行到if外面的时候,str被销毁,slice中的存储也就消失了(注:这是因为slice中存储的是指针和长度)。

2.11 比较器(Comparators)

前面的例子使用的都是默认的排序函数来对键做排序,是对字节做字典排序的。当打开数据库的时候,可以使用自定义的比较器来排序。例如,假设每个数据的键都由两个数字组成,我们应该使用第一个数字来排序,使用第二个数字来打破联系。首先,定义一个合适的leveldb::Comparator的子类。

class TwoPartComparator : public leveldb::Comparator {
 public:
  // Three-way comparison function:
  //   if a < b: negative result
  //   if a > b: positive result
  //   else: zero result
  int Compare(const leveldb::Slice& a, const leveldb::Slice& b) const {
    int a1, a2, b1, b2;
    ParseKey(a, &a1, &a2);
    ParseKey(b, &b1, &b2);
    if (a1 < b1) return -1;
    if (a1 > b1) return +1;
    if (a2 < b2) return -1;
    if (a2 > b2) return +1;
    return 0;
  }

  // Ignore the following methods for now:
  const char* Name() const { return "TwoPartComparator"; }
  void FindShortestSeparator(std::string*, const leveldb::Slice&) const {}
  void FindShortSuccessor(std::string*) const {}
};

现在使用这个比较器来打开数据库:

TwoPartComparator cmp;
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
options.comparator = &cmp;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
....

2.11.1 向后兼容

比较器的Name()方法的返回值在数据库被创建时就已经联系起来了,在之后的每次数据库打开时都会被检查。如果name改变了,leveldb::DB::Open调用时会失败。因此,当且仅当新的键格式和比较函数和已经存在的数据库不兼容时,才会改变name的值,这时不得不丢弃所有已经存在的数据。

当然你也可以通过一些提前的计划来逐步的拓展你的键格式。例如,你可以存储一个版本号在每一个键的末尾(一个字节就够用了)。当你想要改变到新的键格式(例如添加第三个部分到键上)
– (a) 保持同样的比较器名字
– (b) 增加新的键的版本号
– (c) 改变比较函数,使用键的版本号来决定如何解释这些键

2.12 性能

性能可以通过改变配置文件来调节
include/leveldb/options.h

2.13 块大小

leveldb将相邻的键合成一组存储在同一个块中,这样的块是从持久化存储和内存间转移的最小单元。默认的块大小是大约4096个未压缩的字节。如果应用程序总是在数据库中做大量的扫描,可以考虑适当增加这个值的大小。应用程序做大量的很小的值的点读取,可以考虑使用小一点的块大小(如果这些设置确实提升了性能)。如果块大小小于1kilobyte,或者大于几个megabytes是没有太多好处的。注意对于更大的块大小使用压缩会更有效率。

2.14 压缩

每一个数据块在写入持久化存储前会单独的被压缩,压缩是默认的,因为默认的压缩函数是非常快的。对于不可压缩的数据会自动禁用。在极少数的例子中,程序可能会希望完全禁用压缩,仅仅当性能测试显示性能提升了才推荐这么做。

leveldb::Options options;
options.compression = leveldb::kNoCompression;
.... leveldb::DB::Open(options, name, ....) .....

2.15 缓存

数据库的内容被存储在文件系统的文件集合中。每一个文件存储了一系列的压缩的数据块。如果options.cache是non-NULL的,经常使用的未压缩的数据块内容将会被缓存。

#include "leveldb/cache.h"

leveldb::Options options;
options.cache = leveldb::NewLRUCache(100 * 1048576);  // 100MB cache
leveldb::DB* db;
leveldb::DB::Open(options, name, &db);
.... use the db ....
delete db
delete options.cache;

注意缓存保存了未压缩的数据,因此根据程序级的数据量来设置缓存的大小,使用压缩的数据不会有任何的减少。(压缩的数据块的存储留给操作系统缓冲区缓存,或者客户端的其他自定义环境变量来实现)

当执行大量的读取操作时,应用程序最好禁用缓存,这样大量的数据读取不会导致缓存内容大量被改变。per-iterator选项用来实现这个:

leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
  ....
}

2.15.1 键层(Key Layout)

注意硬盘中转移和缓存的单元是一个数据块。相邻的键(根据数据库排序顺序)将被放在同一个数据块中。因此应用程序通过将所有的键依次相邻的排放,将不常访问的值内容(通过key来索引)放在另外一个区域可以提升程序性能。

举例来说,假设我们正在在leveldb上实现一个简单的文件系统,下面的入口类型我们可能希望存储。

filename -> permission-bits, length, list of file_block_ids
file_block_id -> data

我们可能想要设置filename的键前缀为一个字母(‘/’),file_block_id的键前缀为另一个字母(‘0’),因此扫描这些元数据不会强制我们取出和缓存大量的文件内容。

2.16 过滤器

因为leveldb的数据在磁盘上有结构地存放,一个简单的Get()可能调用多个从磁盘的读取。选项 FilterPolicy机制可以被用来减少磁盘读取的次数。

leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
.... use the database ....
delete db;
delete options.filter_policy;

上面的代码将一个基于过滤策略的布隆过滤器(Bloom filter)和数据库联系起来。布隆过滤器的基础过滤依赖于保存每个键的一些位的数据在内存中(在这个例子中每个键有10个bit)。这个过滤器将会减少Get()带来的不必要的磁盘读取的次数大概为100。增加保存的每个键的bit数会更明显的减少读取次数,但是这是在更多的内存消耗的代价上得来的(注:典型的以空间换时间)。我们建议应用程序工作时还剩很多的内存,并且会做大量的随机读取时,设置一个过滤策略。

(注:布隆过滤器是一个针对于大数据量的去重算法,它会通过多个hash将一个字节数组映射到一个很大的bit向量中的k个位置,通过检查一个字节数组hash后的k个位置对应的bit向量是否都为1,如果不是则这个字节数组没有出现过,否则出现过(会有少量的误报)。使用在这里,是为了在读取前检查这个键是否存在,如果不存在则避免了以此全数据库查询,从而大大的提升的性能)

如果你使用一个自定义的比较器,你应该保证你正在使用的过滤策略是和你比较器兼容的。例如,想象一个比较器比较键时忽略了末尾的空间,NewBloomFilterPolicy不能和这样的比较器一起使用。相反的,应用程序需要提供一个自定义的过滤策略同样忽略这些末尾的空间。比如:

class CustomFilterPolicy : public leveldb::FilterPolicy {
 private:
  FilterPolicy* builtin_policy_;

 public:
  CustomFilterPolicy() : builtin_policy_(NewBloomFilterPolicy(10)) {}
  ~CustomFilterPolicy() { delete builtin_policy_; }

  const char* Name() const { return "IgnoreTrailingSpacesFilter"; }

  void CreateFilter(const Slice* keys, int n, std::string* dst) const {
    // Use builtin bloom filter code after removing trailing spaces
    std::vector<Slice> trimmed(n);
    for (int i = 0; i < n; i++) {
      trimmed[i] = RemoveTrailingSpaces(keys[i]);
    }
    return builtin_policy_->CreateFilter(&trimmed[i], n, dst);
  }
};

有一些应用程序可能提供其他过滤策略不使用布隆过滤器,而是使用其他的机制来处理键的集合。细节看leveldb/filter_policy.h

2.17 校验(Checksums)

leveldb会校验所有存储在文件系统中的数据。这里有两个不相关的控制部分提供积极的校验:

ReadOptions::verify_checksums可被设置为true,强制校验从文件系统一次读取的所有数据。默认情况是不会做这个验证的。

Options::paranoid_checks在打开数据库之前可被设置为true,来使得数据库一旦检查到内部错误立马报错。这取决于数据库的哪一部分出错,当数据库被打开时还是其他后来的操作时,这个错误会被警告。默认情况下,偏执检查是关闭的,因此即使有时候持久化存储已经出错了,数据库依然可以被使用。

如果一个数据库出错了(可能它在偏执检查开启时不能打开), leveldb::RepairDB方法会尽可能的修复数据库的数据。

2.18 近似大小

GetApproximateSizes方法可以用来通过一个或多个键范围来获取文件系统已用的空间的近似字节数

leveldb::Range ranges[2];
ranges[0] = leveldb::Range("a", "c");
ranges[1] = leveldb::Range("x", "z");
uint64_t sizes[2];
leveldb::Status s = db->GetApproximateSizes(ranges, 2, sizes);

前面的代码调用将会设置sizes[0]为键范围在[a..c)之间的数据使用的文件系统存储空间的大小,设置sizes[1]为键范围在[x..z)之间的数据使用的文件系统存储空间的大小。

2.19 环境

所有的文件操作(以及其他的系统调用)都在leveldb:Env中实现了。复杂的客户端可能希望提供它们自己的Env的实现来获得更好的控制。例如,一个应用程序可能会在文件IO中引入人为延迟,限制leveldb对系统其他活动的影响。

class SlowEnv : public leveldb::Env {
  .... implementation of the Env interface ....
};

SlowEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ....);

2.20 移植(Porting)

leveldb可能通过提供被leveldb/port/port.h导出的类型/方法/函数/的平台相关的实现,来移植到新的平台。详细内容参考leveldb/port/port_example.h

另外的,新的平台可能需要新的默认leveldb::Env的实现。详细内容参考leveldb/util/env_posix.h

2.21 其他信息

其他信息可以参考其他的文档文件
1. impl.md
2. table_format.md
3. log_format.md

基于Jenkins的自动部署方案

一、前言

在一家小公司工作,负责开发一个项目,前台用的angular2,后台是php和c++。每天下班之前都要将当天的代码部署的线上的开发服务器上,常常也需要更新到生产服务器(项目并没有正式运行,但是老板要用项目去拉投资)。手动部署代码总有一种浪费时间的感觉(因为下班时间到了啊!!!)。使用Jenkins+shell脚本完成自动部署就可以避免这种情况了。感谢赖同学的指导

二、流程介绍

公司内网搭建了git服务器,线上的开发用的服务器部署在阿里云,但是这台服务器的配置实在是太低了,完全没法胜任在线编译部署的需求。所以我要做的是将代码提交到git服务器后,使用Jenkins从git的dev分支拉取代码,构建部署计划(手动或者使用钩子都行)。

三、Jenkins的使用

第一步:部署Jenkins,这一步很简单,直接从Jenkins的官网上wget Jenkins的war包,然后java -jar jenkins.war即可。可以配合supervisor来实现自动启动和重启。

第二步:访问http://ip:8080进行配置,第一次配置的密码是java -jar jenkins.war时,在控制台上输出的密文。然后新建项目,然后选择构建一个多配置项目

第二步:进入到一个配置界面,填一下项目名称,其他的都不用管,直接来到源代码管理。我用的是Git服务器,所以选择git,然后填入仓库地址,配置项目仓库地址可以是HTTP协议,也可以是SSH协议,然后记得在Credentials字段填入认证信息,HTTP协议可以是用户名和密码,SSH的话可以是秘钥。选择的是dev分支。之后在填写构建信息。构建信息构建命令主要就是执行一个脚本

第三步:编写自动部署的脚本,主要就是编译,然后发送到远程服务器,这里用了scp来传输文件,非常好用。

脚本很简单,具体如下:

#!/bin/bash
## 自动部署脚本

cd homepage/mobile
cnpm install
ng build -prod -aot -env=prod
scp -r dist/* root@120.26.103.174:/alidata/www/m_family

注意这里的scp传输文件到远程服务器的前提是,远程服务器上.ssh文件夹下的authorized_keys中有Jenkins所在服务器的公钥,这样才能不需要密码通过认证。

朋友圈式的Timeline设计方案

一、前言

几乎每个人都会发朋友圈、微博,一个看似简单的发布功能,实际在背后是经过精细设计的组织架构。这里有一篇关于新浪微博的架构设计的演讲,主要是讲了通过redis+缓存的使用,来实现发微博的实时性和高效性。

二、Timeline

目前来说,大多数发朋友圈、发微博这种架构的设计都是Timeline的方式。以微博为例,每个用户都有一个自己的Timeline,用户查看微博时,只从自己的Timeline上获取数据。而如果我们是使用普通的sql查询,那么查询可能就是select weibo from table where table.userId in (select userId from guanzhu where guanzhu.userId = 'me')。也就是先查询我所关注的人,再查询所有我关注的人发的微博,然后以时间排序返回。而且因为数据的变化会非常的快,根本没有办法去做缓存,并且刷微博又是一个非常高频的动作,因此普通的sql查询必然无法满足。

而Timeline方案,其实就是以空间换时间的一种方案,假设现在姚晨(1000万粉丝)发布了一条微博,在姚晨的视角里,她已经发布完成了,但是实际上此时并不是她的所有粉丝都能立刻看到这条微博。这个发布动作做了异步处理,此时正在向关注她的微博的粉丝的Timeline上推送(当然微博也不可能真的向她所有粉丝推送,可能会去除一些长期不上线的用户以及僵尸粉)。

因此Timeline方案最复杂的地方就在于发微博这个环节而不是刷新微博这个环节了。发微博相对于刷新微博是非常低频次的,并且只要自己能实时看到就行,并不要求其他人都能实时看到,大大降低了复杂度。

三、使用redis配合完成Timeline的设计。

redis是一款内存数据库,但是它的数据也可以持久化的磁盘上。因为是内存数据库,它最擅长的就是高频次的读取和写入。并且redis支持多种数据结构,其中list数据结构非常符合Timeline的需求。

在redis中,可以创建命名的list(系统中现在使用了zset去保存timeline,比list更优),并且可以对list做push、pop、range操作。比如id为28的用户,他的timeline可以是名为u28的list,然后使用push来向用户的Timeline增加新的推文、使用range来获取指定范围内的推文。

下面的php代码演示了如何从Timeline上获取推文信息

/**
 * 获取一页时间轴上的推文id
 * @param $userId 用户id
 * @param $pageIndex 页码
 * @param $pageSize  页大小
 * @return array 一页postId
 */
public function getTimeline($userId,$pageIndex,$pageSize){
    $end = (1 - $pageIndex) * $pageSize - 1;
    $start = $end - $pageSize + 1;

    $result = $this->redis->lRange($this->getUserTimelineKey($userId),$start,$end);

    return $result;
}

/**
 * 获取用户时间轴上推文的总数
 * @param $userId 用户id
 * @return int 总数
 */
public function getTimelinePostCount($userId){
    return $this->redis->lSize($this->getUserTimelineKey($userId));
}

四、php如何异步推送到其他用户的Timeline

php的机制决定了它无法完成异步操作(一个php文件执行完成之后线程会直接被销毁),因此这里同样是采用了Redis当做任务队列来解耦合,实现异步操作。一个用户发布推文之后,使用下面的setPostTask($pushUserIds,$postId,$userId),将发布推文放到任务队列中然后直接返回。在这里,对用户自己的Timeline做了特殊处理,发推文之后立马给自己的timeline同步推送,其他用户的timeline是异步推送。

        /**
         * 设置推送任务
         * @param $pushUserIds 要推送的userId
         * @param $postId      推文内容
         * @return mix 任务推送状态
         */
        public function setPostTask($pushUserIds,$postId,$userId){

            //为了用户第一时间看到自己发的,这里特殊处理用户自己发的推文
            $this->redis->rPush($this->getUserTimelineKey($userId),$postId);

            $userIds = array();
            foreach($pushUserIds as $value){
                array_push($userIds,$value['userId']);
            }
            $data['u'] = $userIds;
            $data['p'] = $postId;
            $str = \json_encode($data);
            return $this->redis->rPush($GLOBALS['redis_post'],$str);
        }

于此同时,一个以cli模式运行的php脚本从任务队列中顺序获取所有的推送任务,向其他用户的Timeline上推送。

/**
 * 获取推送推文的任务
 * @return mix 推送结果
 */
public function getPostTask(){
    return $this->redis->lPop($GLOBALS['redis_post']);
}

/**
 * 将推文id推送到所有用户的timeline中
 * @TODO 现在是同步的方式
 * @param $pushUserIds 要推送的用户id
 * @param $postId 推文id
 * @return boolean  true操作成功
 */
public function pushToTimeline($pushUserIds,$postId){
    foreach($pushUserIds as $value){
        $result = $this->redis->rPush($this->getUserTimelineKey($value),$postId);
    }
    return true;
}

cli模式运行的php脚本代码:

<?php
/**
 * 从Redis队列中轮询要执行的任务,并执行推送
 *
 */
require_once('vendor/autoload.php');
use DB\PostDB;

$postDB = new PostDB();
$postContent = "";
while(true){

    while( ($postContent = $postDB->getPostTask()) != FALSE ){
        $post = json_decode($postContent,true);
        var_dump($post['u']);
        $postDB->pushToTimeline($post['u'],$post['p']);
    }

    sleep(1);

}

即使这个推送会花费一些时间,但是没有任何用户的体验受到了影响,发布推文的用户timeline因为是特殊处理,所以他能立马看到自己发的推文,其他用户虽然无法立刻看到,但是其他用户对“立刻看到”的需求几乎没有(“立刻看到”指的是刚发布的一瞬间,绝大多数人对于延迟几秒看到根本无所谓)。并且这个架构可以很轻松的横向扩展。

五、一些其他问题

5.1 内存很贵,尽量压缩

redis是内存数据库,内存相对于磁盘的价格要贵上很多倍,因此存在redis中的内容要尽量的小。比如存的是Json,那么Json的字段要尽量的小(直接使用1,2,3,4,5这种也完全可以,即使损失了可读性),同时Json也可以考虑改成谷歌的Protocol Buffers等等。

5.2 timeline里应该存什么

使用Redis存储了每个用户的Timeline,在我的实现中,timeline中存储的是每篇推文的id,然后获取到id之后再去mysql数据库中查询,但是这样的设计虽然大大降低了查询的复杂度,但是并没有降低mysql的IO负荷,如果把推文内容直接存储到Timeline中呢?这样的话如果用户要删除一篇推文,如何对应的删除其他用户的timeline中的推文?这个地方一直是我无法解决的疑惑。

这里有一篇关于twitter使用redis解决timeline的方案,这篇方案中使用的不是list来保存用户的post,而是zset,zset中每条数据由推文的id和时间戳组成,通过时间来排序。仔细考虑一下,list因为是根据推送时间来排序的,可能出现后发的推文出现在靠前的位置(当然我认为是可以通过隐藏描述显示来解决这个问题,因为错位几秒并不影响)。

所以理论上来说,上面那篇文章介绍的方案和我的方案是一致的,只是我把推文的源放在mysql上,twitter直接是存储在redis中来获取更快的速度。

c++11实现的跨平台定时器

一、引言

在一个比较大的系统当中,很多地方都需要定时功能,比如某些存在内存中的数据,需要定时的做持久化(实时持久化可能带来很大的性能问题),如果这个系统要求跨平台。那么实现一个跨平台的定时器就很有必要了。
c++11的标准中,实现了多线程,头文件为<thread>,也有了系统时间相关的库,头文件为<chrono>,这使得使用c++11实现跨平台的定时器有了可能。在这之前,多线程一般都是使用操作系统相关的库。

二、 定时器的基本功能

在定时器的实现之前,需要事先计划好一个定时器的基本功能。大概的功能如下:
1.添加一个定时事件
2.删除一个指定的定时事件
3.定时事件支持只执行一次或者重复执行
4.定时器启动指令
这个部分的内容在Linux 下定时器的实现方式分析中有很详细的介绍

三、 数据结构知识预备

在实现定时器中,需要有一个容器去存储所有的定时事件,比如说最容易想到的数组,如果我们用数组保存定时事件,那么在找出当前需要执行的定时事件的伪代码就是

    for(i = 0 ; i < events_array_size; ++i){
        if(events[i].time <= current_time){
            exec_in_child_thread(events);
        }
    }

这里就是遍历所有的事件,将已经到了执行时间的事件放到子线程中去执行。

在这里有一个可以优化效率问题:注意到每一次查询是否有事件到了执行时间,都会遍历整个数组,也就是O(n)的复杂度,并且查询周期也是非常短的,所以会有非常大的性能问题。因此,我们可以将这里的数组变成有序的,每一次只需查询第一个事件,如果还没到执行时间,那么之后的事件就不用遍历了。这样平均下来就是O(1)的时间复杂度。考虑到我们在插入,删除时始终要保证数组的有序,可以采用最小堆。也就是堆顶始终是最先到执行时间的一项。

四、c++11多线程的知识预备

4.1 join()还是detach()

c++11中创建一个线程很容易

#include <iostream>
#include <thread>

void function_1(){
    std::cout << "hello world" << std::endl;
    while(true){}
}

int main(){
    std::thread t1(function_1);
    t1.join();      //加入运行

    std::cout << "hahah" << std::endl;

    return 0;
}

这段代码就是创建了一个t1的线程,来执行function_1函数。注意到我们这里使用了t1.join(),字面上的意思是将子线程加入的主线程来执行,这样导致的结果是主线程会在t1.join()这里停止,直到t1执行结束后再往下执行。在这里function_1中存在死循环,所以hahah永远不会被输出。而如果使用t1.detach()的话,即将子线程从主线程分离出去,那么主线程就会一直向下执行,在这里就会执行到return 0,然后主线程销毁,detach出去的子线程可能就没有任何显示的机会了(查过资料说detach出去子线程的行为的undefined的)。

在这个定时器的实现中,当然不能使用join(),因为会阻塞程序的正常运行。

4.2 如何使得定时器的使用是线程安全的。

在一个程序中,可能很多地方会使用到定时器去触发某个动作,但是为了效率,肯定不会创建多个定时器实例,因此定时器的实现应该是单例的。因为需要单例,因此定时器的创建,定时器中定时事件的添加和删除都是线程安全的(因为同一时间可能会有多个线程操作定时器)。

4.2.1 线程安全的单例模式

单例模式的实现方法有很多种,因为能力问题,不对这一点多做叙述。可以看这一篇单例模式(Singleton)及其C++实现

我这里使用的是使用static来创建局部可见的全局变量

static Timer* getInstance(std::chrono::milliseconds tick){
    static Timer timer(tick);
    return &timer;
}

4.2.2 使用锁保证定时事件的添加和删除是线程安全的。

在往定时器中添加和删除定时事件时,就是在做最小堆的添加和删除。这个中间线程不安全的现象为:以添加为例,往最小堆数组末尾加入一个数据,然后再逐层向上调整。如果在这个调整的过程中,另外一个线程执行了删除操作,两个调整过程就可能同时发生,造成不可预知的错误,使得最小堆并不有序。

所以添加和删除操作应该是互斥的,只有其中一个操作执行结束,另一个操作才可以开始。c++中可以使用信号量mutex和锁locker轻松的完成互斥的需求。示例代码如下:

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>

class LofFile{
    public:
        LofFile(){
            f.open("log.txt");
        }

        void shared_print(std::string id,int value){
            std::lock(m_mutex,m_mutex2);
            std::lock_guard<std::mutex> locker(m_mutex,std::adopt_lock);
            std::lock_guard<std::mutex> locker2(m_mutex2,std::adopt_lock);
            std::cout << "from" << id << ":" << value << std::endl;
        }

        void shared_print2(std::string id,int value){
            std::lock(m_mutex,m_mutex2);
            std::lock_guard<std::mutex> locker2(m_mutex2,std::adopt_lock);      //lock_guard是为了防止语句执行中出现异常,锁不被释放。这样做可以保证在退出代码块后解锁
            std::lock_guard<std::mutex> locker(m_mutex,std::adopt_lock);
            std::cout << "from" << id << ":" << value << std::endl;
        }

    protected:
    private:
        std::mutex m_mutex;     //使用信号量来解决资源竞争
        std::mutex m_mutex2;
        std::ofstream f;

};

void function_1(LofFile& log){
    for(int i = 0; i > -100; i--){
        log.shared_print("From t1:",i);
    }
};

int main(){
    LofFile log;
    std::thread t1(function_1,std::ref(log));//线程开始运行
    for(int i = 0; i < 100; i++){
        log.shared_print2("From main",i);
    }

    t1.join();
    return 0;
}

五、总结与实现

分析到这里,一个定时器的基本要素已经具备了。
1.用来存储定时事件,并且添加和删除操作都是线程安全的最小堆
2.可以多线程实现定时事件的执行从而不阻塞主线程。

下面贴出所有的实现代码:

5.1 最小堆的实现。

SortedHeap.hpp

/**
 * 排序堆的实现
 * 堆使用的是完全二叉树的结构,使用数组(数组从零开始)保存,那么最后一个非叶子节点为(n - 1) / 2
 * 堆的构建一直都是在有序的基础上的,那么每次调整只需比较i和(i - 1) / 2的元素,依次上推
 * 支持任意类的排序
 * 当前还不支持多线程环境下的使用
 * author:jiangpengfie
 * date:2017-05-09
 */
#ifndef SORTEDHEAP_H
#define SORTEDHEAP_H
#include <iostream>
#include <vector>
#include <functional>
#include <memory>
#include <mutex>
#include <condition_variable>
#include "src/core/util/util.h"


template<class T>
class SortedHeap{
    private:
        struct HeapNode{
            unsigned int id;
            T obj;
            HeapNode(unsigned int id,T t):obj(t){
                this->id = id;
            }
        };
        std::vector<HeapNode> heap;
        unsigned int autoIncrementId;
        std::function<bool(T& ,T&)> cmp;    //比较函数,实现选择构造最大堆还是最小堆
        std::mutex mu1;                
        std::mutex mu2;                

        /**
         * 插入节点后调整堆中不符合的节点
         */
        void adjustAfterInsert();

        /**
         * pop出堆顶元素后调整堆中不符合的节点
         */
        void adjustAfterPopTop();

        /**
         * 删除节点后调整堆中不符合的节点
         * @param i 删除的节点id
         */
        void adjustAfterDelete(int id);         

        void swap(HeapNode& t1,HeapNode& t2);

        void deleteNodeByPos(const unsigned int pos);
    public:
        /**
         * 构造函数
         * @param cmp 用来比较
         */
        SortedHeap(std::function<bool(T&,T&)> cmp);
        /**
         * 插入节点
         * @param node 插入的节点
         */
        unsigned int insertNode(T& node);
        /**
         * 删除节点,时间复杂度为O(n)
         * @param id  要删除的节点id
         */
        void deleteNode(unsigned int id);

        /**
         * pop最小的节点
         * @return T* 返回的最顶部的节点指针
         */
        std::unique_ptr<T> popTopNode();

        /**
         * 获取最顶部的节点
         * @return T 最顶部的节点指针
         */
        std::unique_ptr<T> getTopNode();

        /**
         * 删除顶部的节点
         *
         */
        void deleteTopNode();
};

template<typename T>
SortedHeap<T>::SortedHeap(std::function<bool(T&,T&)> cmp){
    this->cmp = cmp;
    this->autoIncrementId = 0;
}

template<typename T>
void SortedHeap<T>::swap(HeapNode& t1,HeapNode& t2){
    HeapNode tmp = t1;
    t1 = t2;
    t2 = tmp;
}

template<typename T>
void SortedHeap<T>::adjustAfterInsert(){
    int last = this->heap.size() - 1;
    int flag = true;
    //从插入的节点位置开始向上调整
    while(last > 0 && flag){
        if(this->cmp(this->heap[last].obj,this->heap[(last - 1) / 2].obj)){
            this->swap(this->heap[(last - 1) / 2],this->heap[last]);
        }else{
            //不需要调整了
            flag = false;
        }
        last = (last - 1) / 2;
    }
}

template<typename T>
void SortedHeap<T>::adjustAfterDelete(int pos){
    //从pos位置开始向下调整
    int last = this->heap.size() - 1;
    if(last == 0)
        return;     //最后一个不需要调整
    bool flag = true;   //标记是否需要调整
    while(pos <= (last - 1) / 2 && flag){
        //一直调整到最后一个非叶子结点
        int topNum = 0;     //记录最小的结点编号

          //(pos + 1) * 2 - 1是左孩子,pos是父
        if(this->cmp(this->heap[(pos + 1) * 2 - 1].obj,this->heap[pos].obj)){
            topNum = (pos + 1) * 2 - 1;
        }else{
            topNum = pos;
        }

        if((pos + 1) * 2 <= last){
            //如果存在右结点
            if(this->cmp(this->heap[(pos + 1) * 2].obj,this->heap[topNum].obj)){
                topNum = (pos + 1) * 2;
            }
        }

        //看看topNum是不是自己
        if(pos == topNum){
            //是自己就不用调整了
            flag = false;
        }else{
            //交换
            this->swap(this->heap[pos],this->heap[topNum]);
        }
        pos = topNum;
    }
}


template<typename T>
void SortedHeap<T>::deleteNodeByPos(const unsigned int pos){
    unsigned int last = this->heap.size() - 1;
    if(pos > last){
        return;
    }
    std::lock(mu1,mu2);             //上锁
    std::lock_guard<std::mutex> locker1(mu1,std::adopt_lock);
    std::lock_guard<std::mutex> locker2(mu2,std::adopt_lock);
    //与最后一个交换
    swap(this->heap[pos],this->heap[last]);
    //删除最后一个
    this->heap.pop_back();      

    this->adjustAfterDelete(pos);
}



template<typename T>
unsigned int SortedHeap<T>::insertNode(T& node){
    HeapNode hNode(this->autoIncrementId++,node);
    std::lock(mu1,mu2);             //上锁
    std::lock_guard<std::mutex> locker1(mu1,std::adopt_lock);
    std::lock_guard<std::mutex> locker2(mu2,std::adopt_lock);
    this->heap.push_back(hNode);     //先将node放在最后一位
    if(this->heap.size() != 1){
        //如果大小不等于1,则在新增节点后调整
        this->adjustAfterInsert();
    }
    return this->autoIncrementId - 1;
}


template<typename T>
void SortedHeap<T>::deleteNode(unsigned int id){
    for(unsigned int i = 0; i < this->heap.size(); i++){
        if(heap[i].id == id){
            //找到了id
            this->deleteNodeByPos(i);
            break;
        }
    }


}

template<typename T>
std::unique_ptr<T> SortedHeap<T>::popTopNode(){
    if(this->heap.size() != 0){
        std::unique_ptr<T> top(new T(this->heap[0].obj));
        this->deleteNodeByPos(0);
        return top;
    }else{
        std::unique_ptr<T> p = nullptr;
        return p;
    }
}

template<typename T>
std::unique_ptr<T> SortedHeap<T>::getTopNode(){
    if(this->heap.size() != 0){
        std::unique_ptr<T> top(new T(this->heap[0].obj));
        return top;
    }else{
        std::unique_ptr<T> p = nullptr;
        return p;
    }
}

template<typename T>
void SortedHeap<T>::deleteTopNode(){
   if(this->heap.size() != 0){
        this->deleteNodeByPos(0);
    }
}

#endif



5.2 定时器的实现

Timer.h

/**
 * 定时器的实现
 * 支持int setTimer(T interval,function action):设置一个定时器,指定间隔interval和回调函数action,返回定时器id
 * 支持void deleteTimer(int timerId):删除一个定时器
 * 数据结构:最小堆模型,按照定时器触发的时间排序
 * author:jiangpengfei
 * date:2017-05-09
 */
#ifndef TIMER_H
#define TIMER_H
#include <iostream>
#include <chrono>
#include <functional>
#include <thread>
#include <memory>
#include "SortedHeap.hpp"

class Timer{
    private:
        std::chrono::milliseconds tick;
        double timeline;     //当前时间线,long double的字节数为12
        bool isStart;        //标志当前定时器的启动状态
        struct SchedulerEvent{
          unsigned int id;                   //定时事件的唯一标示id
          double interval;                   //事件的触发间隔,在重复事件中会用到这个属性
          double deadline;                   //定时事件的触发时间
          std::function<void()> action;      //触发的事件
          bool isRepeat;                     //是否是重复执行事件
          SchedulerEvent( double interval, double timeline,std::function<void()> action,bool isRepeat){
              this->interval = interval;
              this->deadline = interval + timeline;
              this->action = action;
              this->isRepeat = isRepeat;
          }
        };

        SortedHeap<SchedulerEvent> eventQueue;

        /**
         * 执行到达期限的定时器
         */
        void loopForExecute();

        //私有的构造函数
        Timer(std::chrono::milliseconds tick):eventQueue(
            [](SchedulerEvent& a,SchedulerEvent& b){
                return a.deadline < b.deadline;
            }
        ){
            this->timeline = 0;
            this->tick = tick;
            this->isStart = false;
        }

    public:

        //单例模式
        static Timer* getInstance(std::chrono::milliseconds tick){
            static Timer timer(tick);
            return &timer;
        }

        /**
         * 设置定时器
         * @param interval 定时间隔
         * @param action 定时执行的动作
         * @param isRepeat 是否重复执行,默认不重复执行
         * @return unsigned int 定时器的id,可以根据这个id执行删除操作
         */
        unsigned int addEvent(double interval,std::function<void()> action,bool isRepeat = false);

        /**
         * 删除定时器
         * @param timerId 定时器id
         *
         */
        void deleteEvent(unsigned int timerId);

        /**
         * 同步执行启动定时器
         */
         void syncStart();

         /**
         * 异步执行启动定时器
         */
         void asyncStart();

};


#endif

Timer.cpp

#include "src/core/util/Timer.h"

unsigned int Timer::addEvent(double interval,std::function<void()> action,bool isRepeat){
    SchedulerEvent event(interval,this->timeline,action,isRepeat);
    return this->eventQueue.insertNode(event);
}

void Timer::deleteEvent(unsigned int timerId){
    this->eventQueue.deleteNode(timerId);
}

void Timer::loopForExecute(){
    std::unique_ptr<SchedulerEvent> top = this->eventQueue.getTopNode();
    while(top != nullptr && top->deadline <= this->timeline){
        //如果已经到了执行的时间,新开一个子线程执行任务
        std::thread t(top->action);
        t.detach();    //子线程分离

        if(top->isRepeat){
            //如果是重复事件,则重新添加
            this->addEvent(top->interval,top->action,top->isRepeat);
        }

        //从堆中删除
        this->eventQueue.deleteTopNode();
        top = this->eventQueue.getTopNode();
    }
    //执行一次后等待一个周期
    std::this_thread::sleep_for(this->tick);
    //周期增1
    this->timeline++;
}

void Timer::asyncStart(){
    if(!this->isStart){
        std::thread daemon_thread(&Timer::syncStart,this);
        daemon_thread.detach();     //从当前主线程分离
    }
}

void Timer::syncStart(){
    if(!this->isStart){
        while(1)
            this->loopForExecute();
    }
}

测试执行的代码

#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <string>
#include <functional>
#include <thread>
#include <memory>
#include <fstream>
#include "src/core/util/Timer.h"

void myprint(std::string msg){
    std::ofstream of("timer.txt", std::ios::app);
    std::thread::id this_id = std::this_thread::get_id();
    auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    of << "From Thread " << this_id << "at time " << std::put_time(std::localtime(&t), "%Y-%m-%d %H.%M.%S") << ":" << msg << std::endl;
}

int main(){
    std::chrono::milliseconds tick(2000);       //1000毫秒作为一个周期
    Timer* timer = Timer::getInstance(tick);
    std::function<void()> f1 = std::bind(myprint,"第一个加入,10tick后执行");
    std::function<void()> f2 = std::bind(myprint,"第二个加入,被删除不执行");
    std::function<void()> f3 = std::bind(myprint,"第三个加入,每5tick重复执行");
    std::function<void()> f4 = std::bind(myprint,"第四个加入,5tick后执行");
    std::function<void()> f5 = std::bind(myprint,"第五个加入,5tick后执行");
    std::function<void()> f6 = std::bind(myprint,"第六个加入,5tick后执行");
    std::function<void()> f7 = std::bind(myprint,"第七个加入,5tick后执行");
    std::function<void()> f8 = std::bind(myprint,"第八个加入,5tick后执行");
    std::function<void()> f9 = std::bind(myprint,"第九个加入,5tick后执行");
    std::function<void()> f10 = std::bind(myprint,"第十个加入,5tick后执行");
    std::function<void()> f11 = std::bind(myprint,"第十一个加入,15tick后执行");
    std::function<void()> f12 = std::bind(myprint,"第十二个在执行后加入,20tick+5s后执行");

    timer->addEvent(10,f1);
    int id = timer->addEvent(11,f2);
    timer->addEvent(5,f3,true);
    timer->addEvent(5,f4);
    timer->addEvent(5,f5);
    timer->addEvent(5,f6);   
    timer->addEvent(5,f7);
    timer->addEvent(5,f8);
    timer->addEvent(5,f9);
    timer->addEvent(5,f10);
    timer->addEvent(15,f11);

    timer->deleteEvent(id);

    myprint("线程开始启动,每tick是2秒");

    //异步执行,程序退出后计时器也会终止,因此在下面使用while循环保证程序不会退出
    timer->asyncStart();
    //timer->syncStart();


    //休眠5秒钟
    std::this_thread::sleep_for(std::chrono::seconds(5));   
    //应该在大概20*tick+5秒后执行,
    //TODO 执行后加入的定时器不对
    timer->addEvent(20,f12);

    getchar();

    return 0;
}

stream和buffer的概念解析

1.概念解析

buffer:内存中一块确定的临时存储区域。
stream:一段不确定长度的数据序列,可以认为stream就是I/O中input部分的FIFO实现,为了方便理解,可以用键盘输入作为输入来举例,从键盘中敲入的字符组成一段stream,先敲入的字符先被读取,也就是FIFO,但是读取的过程中,你并不知道总共要读取多少,也就是长度的不确定性。

2.具体使用时的区别与联系

在使用 stream 的过程中,我们遵循这样一个基本的过程,以从文件中读取为例:
1.程序向 stream 发起请求,要读取一个字节。
2.stream 在磁盘上定为到请求的字节,并发送给程序。
3.程序得到这个字节后,在进行请求,重复到第一步

这个样子就有一个很严重的效率问题,读取一个字节需要在程序和stream之间来回一次,那么1000个字节就是1000次。怎么解决这个问题呢?

这时候就到 buffer 出场了,buffer 根据上面的概念,他是内存中一块确定的临时存储区域。如果使用一个 1000 字节的 buffer ,那么程序和 stream 的关系就变成:
1.程序向stream发起请求,要求stream将buffer填满。
2.stream向buffer中填充字节,要么填满1000个字节为止,要么stream到达结尾为止。
3.程序从buffer中一次性获取1000字节的数据

很明显,使用buffer的好处在于减少的请求IO的次数,也大大提升了效率

LINUX下修改php.ini配置报错输出

首先是找到php.ini文件

输入

find / -name php.ini

总共有两个结果

/etc/php5/cli/php.ini
/etc/php5/apache2/php.ini

cli/php.ini指的是在控制台环境下运行php脚本使用的配置文件

apache2/php.ini是apache2环境下运行php脚本使用的配置文件

如果你还不确定是用哪一个,可以新建一个php代码

使用 echo phpinfo(); 来输出php信息,其中有一项是加载的php.ini路径。我的就是apache2/php.ini

然后编辑apache2/php.ini,我这里是要开启他的报错,不然如果代码中有错,浏览器访问就会直接报500错误

找到display_errors这一行,去除前面的;

display_errors=On

配置错误级别

error_reporting=E_ALL & ~E_NOTICE

修改完成后要使php.ini配置生效,网上普遍的说法是重启apache服务生效

service apache2 restart

代码出错的地方仍然报500错误

准备去看apache的日志文件,同样使用find / -name error.log找到日志,由于日志太多,就把之前的error.log删掉,重启apache,浏览器打开出错页面,然后查看error.log。发现日志中是记录了代码出错的信息的。确定就是没有开启报错,导致500错误。

修改apache2.conf,在/etc/apache2下面。

添加

php_flag display_errors on
php_value error_reporting 2039

重启apache
service apache2 restart

浏览器打开错误页面,成功报错!

linux下一些好用的指令记录

查找当前目录下包含指定字符串的文件

grep -rn “hello,world!” *

  • : 表示当前目录所有文件,也可以是某个文件名
    -r 是递归查找
    -n 是显示行号
    -R 查找所有文件包含子目录
    -i 忽略大小写

mysql数据库相关操作
创建用户
mysql> insert into mysql.user(Host,User,Password) values(“localhost”,”phplamp”,password(“1234”));
授权

grant all privileges on phplampDB.* to phplamp@localhost identified by ‘1234’;
删除用户
mysql>DELETE FROM user WHERE User=”phplamp” and Host=”localhost”;
mysql>flush privileges;
修改密码
mysql>update mysql.user set password=password(‘新密码’) where User=”phplamp” and Host=”localhost”;
mysql>flush privileges;
刷新系统权限表
mysql>flush privileges;

cinnamon桌面图标重复
gsettings set org.gnome.desktop.background show-desktop-icons false

java反射机制以及在android开发中的应用

java中的反射机制:只要给定类的名字就可以得到所有类的信息。因为这个类的名字是可以在代码运行时动态指定的,所以利用java的反射机制比通过new的方式要灵活的多

在java中,通过new的方式创建的对象称为静态加载(编译时加载类),通过反射机制创建对象称为动态加载(运行时加载类)

java反射机制的使用方式

这里有个Human类如下

package com.myway5;

public class Human {
private String name;
private int age;
public String word;

public Human() {
    // TODO Auto-generated constructor stub
}

public Human(String name) {
    this.name = name;
}

public Human(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}
public void speak(String word) {
     System.out.println(word);
 }

}
我们使用反射机制获取Human类所有的信息

package com.myway5;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Client {
public static void main(String[] args) {
// 获取类名等信息
Class class1 = Human.class;
System.out.println(class1.getName());
System.out.println(class1.getSimpleName());

    System.out.println("***********构造方法**********");

    Constructor[] cs = class1.getDeclaredConstructors();
    for (Constructor constructor : cs) {
        System.out.print(constructor.getName() + "(");
        Class[] paramsType = constructor.getParameterTypes();
        for (Class class2 : paramsType) {
            System.out.print(class2.getName() + ",");
        }
        System.out.println(")");
    }

    System.out.println("**********公共成员变量************");
    Field[] fields = class1.getFields();
    for (Field field : fields) {
        System.out.println(field.getType().getName() + ":" + field.getName());
    }

    System.out.println("**********公共方法**************");
    Method[] methods = class1.getMethods();
    for (Method method : methods) {
        System.out.print(method.getName() + "(");
        Class[] params = method.getParameterTypes();
        for (Class class3 : params) {
            System.out.print(class3.getName() + ",");
        }
        System.out.println(method.getReturnType().getName() + ")");
    }

}

}
输出结果如下:
com.myway5.Human
Human
***********构造方法**********
com.myway5.Human(java.lang.String,int,)
com.myway5.Human(java.lang.String,)
com.myway5.Human()
**********公共成员变量************
java.lang.String:word
**********公共方法**************
getName(java.lang.String)
setName(java.lang.String,void)
getAge(int)
setAge(int,void)
wait(long,int,void)
wait(long,void)
wait(void)
equals(java.lang.Object,boolean)
toString(java.lang.String)
hashCode(int)
getClass(java.lang.Class)
notify(void)
notifyAll(void)
可以看到,所有公共的成员变量,方法都可以通过这个方式获取到,那么怎么调用其中的方法呢
public static void useMethod() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
InstantiationException, NoSuchMethodException, SecurityException {
Class class1 = Human.class;
Human man = (Human) class1.newInstance();//通过newInstance创建实例
Method method = class1.getMethod(“speak”, String.class);
Object object = method.invoke(man, new Object[] { “hello reflect” });
}

android开发中的使用后续更新

继上文更新:

android开发中常常会使用java的反射机制来更改一些系统底层无法更改的代码逻辑。比如说在AlertDialog的使用中,通过自带的setPositiveButton或者setNegativeButton时,一旦点击按钮都会退出dialog,有时候我们不希望他退出,比如用户登录时登录失败再次登录。那么一种解决办法 就是通过java的反射机制。

package com.myway5.java_reflect;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class MainActivity extends AppCompatActivity{

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    AlertDialog.Builder builder=new AlertDialog.Builder(this);

    builder.setMessage("Hello Dialog")
             .setTitle("对话框")
                .setView(getLayoutInflater().inflate(R.layout.dialog_signin,null));
    builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

        }
    });
    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

        }
    });
    AlertDialog dialog=builder.create();
    try {
        Field field = dialog.getClass().getDeclaredField("mAlert");
        field.setAccessible(true);
        Object obj=field.get(dialog);
        field=obj.getClass().getDeclaredField("mHandler");
        field.setAccessible(true);
        field.set(obj,new ButtonHandler(dialog));
    }catch (Exception e){
        e.printStackTrace();
    }
    dialog.show();
}
private static final class ButtonHandler extends Handler {
    // Button clicks have Message.what as the BUTTON{1,2,3} constant
    private static final int MSG_DISMISS_DIALOG = 1;

    private WeakReference<DialogInterface> mDialog;

    public ButtonHandler(DialogInterface dialog) {
        mDialog = new WeakReference<DialogInterface>(dialog);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

            case DialogInterface.BUTTON_POSITIVE:
            case DialogInterface.BUTTON_NEGATIVE:
            case DialogInterface.BUTTON_NEUTRAL:
                ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
                break;

            case MSG_DISMISS_DIALOG:
                //这里是点击后的逻辑,因为注释掉了dismiss(),dialog不会退出了
                //((DialogInterface) msg.obj).dismiss();
        }
    }
}

}
其中
try {
Field field = dialog.getClass().getDeclaredField(“mAlert”);
field.setAccessible(true);
Object obj=field.get(dialog);
field=obj.getClass().getDeclaredField(“mHandler”);
field.setAccessible(true);
field.set(obj,new ButtonHandler(dialog));
}catch (Exception e){
e.printStackTrace();
}
这个部分就是通过反射获取AlertDialog的私有变量mAlert,然后获取mAlert的成员变量mHandler,将这个Handler设置成我们自己的ButtonHandler,这样就解决了点击后退出的问题

代码地址:https://github.com/joyme123/java_reflect

参考文章:http://www.oschina.net/question/163910_27112

android studio导入项目出现Gradle version 1.10 is required. Current version is 2.0的解决办法

1.首先在项目下找到gradlew.bat这个文件,单击运行。它会下载Gradle 1.10 。等待任务完成

2.在项目的报错的控制台中选择setting这个选项。然后设置本地的Gradle。我的是默认放在

C:\Users\joyme.gradle\wrapper\dists\gradle-1.10-all\bif7dlrky2i400uf9zsz2c0my\gradle-1.10

vitamio使用出现找不到文件的解决方法

首先我遇到这个问题肯定不是一般的找不到文件的错误。使用vitamio的VideoView的时候,出现一部分视频会找不到文件。

分析很久问题出现的原因,最后得出是因为这部分视频名都是URLEncode过的,可能是这个原因导致字符串在解析的过程中出现问题。然后跟踪代码到VideoView的setVideoPath处,发现他的实现如下

public void setVideoPath(String path) {
setVideoURI(Uri.parse(path));
}
很明显这里就是导致错误所在,所以解决办法就是将string 类型的path先编码一下

videoView.setVideoPath(URLEncoder.encode(path, “UTF-8”));