• memcached的最佳实践方案


    基本问题

    1、memcached的基本设置 
    1)启动Memcache的服务器端 
    # /usr/local/bin/memcached -d -m 10 -u root -l 192.168.0.200 -p 12000 -c 256 -P /tmp/memcached.pid

    -d选项是启动一个守护进程, 
    -m是分配给Memcache使用的内存数量,单位是MB,我这里是10MB, 
    -u是运行Memcache的用户,我这里是root, 
    -l是监听的服务器IP地址,如果有多个地址的话,我这里指定了服务器的IP地址192.168.0.200, 
    -p是设置Memcache监听的端口,我这里设置了12000,最好是1024以上的端口, 
    -c选项是最大运行的并发连接数,默认是1024,我这里设置了256,按照你服务器的负载量来设定, 
    -P是设置保存Memcache的pid文件,我这里是保存在 /tmp/memcached.pid,

    2)如果要结束Memcache进程,执行:

    # kill `cat /tmp/memcached.pid`

    哈希算法任意长度的二进制值映射为固定长度的较小二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该

    段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的。

    2、一致性Hash算法的目的有两点:一是节点变动后其他节点受影响尽可能小;二是节点变动后数据重新分配尽可能均衡 。

    3、为什么要运行 memcached ?

    如果网站的高流量很大并且大多数的访问会造成数据库高负荷的状况下,使用 memcached 能够减轻数据库的压力。

    4、适用memcached的业务场景?

    1)如果网站包含了访问量很大的动态网页,因而数据库的负载将会很高。由于大部分数据库请求都是读操作,那么memcached可以显著地减小数据库负载。

    2)如果数据库服务器的负载比较低但CPU使用率很高,这时可以缓存计算好的结果( computed objects )和渲染后的网页模板(enderred templates)。

    3)利用memcached可以缓存session数据、临时数据以减少对他们的数据库写操作。

    4)缓存一些很小但是被频繁访问的文件。

    5)缓存Web 'services'(非IBM宣扬的Web Services,译者注)或RSS feeds的结果.。

    5、不适用memcached的业务场景?

    1)缓存对象的大小大于1MB

    Memcached本身就不是为了处理庞大的多媒体(large media)和巨大的二进制块(streaming huge blobs)而设计的。

    2)key的长度大于250字符

    3)虚拟主机不让运行memcached服务

         如果应用本身托管在低端的虚拟私有服务器上,像vmware, xen这类虚拟化技术并不适合运行memcached。Memcached需要接管和控制大块的内存,如果memcached管理的内存

    被OS或 hypervisor交换出去,memcached的性能将大打折扣。

    4)应用运行在不安全的环境中

    Memcached为提供任何安全策略,仅仅通过telnet就可以访问到memcached。如果应用运行在共享的系统上,需要着重考虑安全问题。

    5)业务本身需要的是持久化数据或者说需要的应该是database

    6、能够遍历memcached中所有的item吗?

    不能,这个操作的速度相对缓慢且阻塞其他的操作(这里的缓慢时相比memcached其他的命令)。memcached所有非调试(non-debug)命令,例如add, set, get, fulsh等无论

    memcached中存储了多少数据,它们的执行都只消耗常量时间。任何遍历所有item的命令执行所消耗的时间,将随着memcached中数据量的增加而增加。当其他命令因为等待(遍历所

    有item的命令执行完毕)而不能得到执行,因而阻塞将发生。

    集群的相关问题

    7、memcached是怎么工作的?

    Memcached的高性能源于两阶段哈希(two-stage hash)结构。Memcached就像一个巨大的、存储了很多<key,value>对的哈希表。通过key,可以存储或查询任意的数据。 客户端

    可以把数据存储在多台memcached上。当查询数据时,客户端首先参考节点列表计算出key的哈希值(阶段一哈希),进而选中一个节点;客户端将请求发送给选中的节点,然后

    memcached节点通过一个内部的哈希算法(阶段二哈希),查找真正的数据(item)并返回给客户端。从实现的角度看,memcached是一个非阻塞的、基于事件的服务器程序。

    8、memcached最大的优势是什么?

    Memcached最大的好处就是它带来了极佳的水平可扩展性,特别是在一个巨大的系统中。由于客户端自己做了一次哈希,那么我们很容易增加大量memcached到集群中。memcached

    之间没有相互通信,因此不会增加 memcached的负载;没有多播协议,不会网络通信量爆炸(implode)。

    9、memcached和MySQL的query cache相比,有什么优缺点?

    缺点:

    1)相比MySQL的query cache,把memcached引入应用中需要不少的工作量。MySQL的query cache,可以自动地缓存SQL查询的结果,被缓存的SQL查询可以被反复、快速的执行。

    优点:

    1)当修改表时,MySQL的query cache会立刻被刷新(flush)。当写操作很频繁时,MySQL的query cache会经常让所有缓存数据都失效。

    2)在多核CPU上,MySQL的query cache会遇到扩展问题(scalability issues)。在多核CPU上,query cache会增加一个全局锁(global lock), 由于需要刷新更多的缓存数据,速度

    会变得更慢。

    3)在MySQL的query cache中,是不能存储任意的数据的(只能是SQL查询结果)。利用memcached,我们可以搭建出各种高效的缓存。比如,可以执行多个独立的查询,构建出一个

    用户对象(user object),然后将用户对象缓存到memcached中。而query cache是SQL语句级别的,不可能做到这一点。在小的网站中,query cache会有所帮助,但随着网站规模的

    增加,query cache的弊将大于利。

    4)query cache能够利用的内存容量受到MySQL服务器空闲内存空间的限制。给数据库服务器增加更多的内存来缓存数据,固然是很好的。但是,有了memcached,只要您有空闲的内

    存,都可以用来增加memcached集群的规模,然后您就可以缓存更多的数据。

    10、memcached和服务器的local cache(比如PHP的APC、mmap文件等)相比,有什么优缺点?

    1)首先,local cache面临着严重的内存限制,能够利用的内存容量受到(单台)服务器空闲内存空间的限制。

    2)local cache有一点比memcached和query cache都要好,那就是它不但可以存储任意的数据,而且没有网络存取的延迟。因此,local cache的数据查询更快。考虑把highly

    common的数据放在local cache中吧。如果每个页面都需要加载一些数量较少的数据,可以考虑把它们放在local cached。

    3)local cache缺少集体失效(group invalidation)的特性。在memcached集群中,删除或更新一个key会让所有的观察者觉察到。但是在local cache中, 我们只能通知所有的服务器

    刷新cache(很慢,不具扩展性)或者仅仅依赖缓存超时失效机制。

    11、memcached的cache机制是怎样的?

    Memcached主要的cache机制是LRU(最近最少用)算法+超时失效。当您存数据到memcached中,可以指定该数据在缓存中可以呆多久Which is forever, or some time in the

    future。如果memcached的内存不够用了,过期的slabs会优先被替换,接着就轮到最老的未被使用的slabs。

    12、memcached如何实现冗余机制?

    不实现!Memcached应该是应用的缓存层,从设计本身来京就不带有任何冗余机制。如果一个memcached节点失去了所有数据,应该可以从数据源(比如数据库)再次获取到数据。应

    用系统应该可以容忍节点的失效。如果担心节点失效会大大加重数据库的负担,那么可以采取一些办法。比如您可以增加更多的节点(来减少丢失一个节点的影响),热备节点(在其他节

    点down了的时候接管IP)等等。

    13、memcached如何处理容错的?

    在节点失效的情况下,集群没有必要做任何容错处理。如果发生了节点失效,应对的措施完全取决于用户。

    节点失效时,下面列出几种方案供您选择:

    1)忽略它! 在失效节点被恢复或替换之前,还有很多其他节点可以应对节点失效带来的影响。

    2)把失效的节点从节点列表中移除。做这个操作千万要小心!在默认情况下(余数式哈希算法),客户端添加或移除节点,会导致所有的缓存数据不可用!因为哈希参照的节点列表变化

    了,大部分key会因为哈希值的改变而被映射到(与原来)不同的节点上。

    3)启动热备节点,接管失效节点所占用的IP。这样可以防止哈希紊乱(hashing chaos)。

    4)如果希望添加和移除节点,而不影响原先的哈希结果,可以使用一致性哈希算法(consistent hashing)。

    5)两次哈希(reshing)。当客户端存取数据时,如果发现一个节点down了,就再做一次哈希(哈希算法与前一次不同),重新选择另一个节点(需要注意的时,客户端并没有把down

    的节点从节点列表中移除,下次还是有可能先哈希到它)。如果某个节点时好时坏,两次哈希的方法就有风险了,好的节点和坏的节点上都可能存在脏数据(stale data)。

    14、如何将memcached中item批量导入导出?

    不应该这样做!Memcached是一个非阻塞的服务器。任何可能导致memcached暂停或瞬时拒绝服务的操作都应该值得深思熟虑。向memcached中批量导入数据往往不是您真正想要

    的!想象看,如果缓存数据在导出导入之间发生了变化,您就需要处理脏数据了;如果缓存数据在导出导入之间过期了,您又怎么处理这些数据呢?

    因此,批量导出导入数据并不像想象中的那么有用。不过在一个场景倒是很有用。如果您有大量的从不变化 的数据,并且希望缓存很快热(warm)起来,批量导入缓存数据是很有帮助

    的。

    15、但是我确实需要把memcached中的item批量导出导入,怎么办??

    如果需要批量导出和导入,最可能的原因一般是重新生成缓存数据需要消耗很长的时间或者数据库坏了让您饱受痛苦。

    如果一个memcached节点down了让您很痛苦,那么必须对数据库做一些优化工作。比如处理"惊群"问题( memcached节点都失效了,反复的查询让数据库不堪重负)或者存在优化不

    好的查询等。Memcached 并不是逃避优化查询的借口和方案。

    这里给出一些提示:

    使用MogileFS(或者CouchDB等类似的软件)在存储item,把item计算出来并dump到磁盘上。MogileFS可以很方便地覆写item,并提供快速地访问。甚至可以把MogileFS中的item

    缓存在memcached中,这样可以加快读取速度。 MogileFS+Memcached的组合可以加快缓存不命中时的响应速度,提高网站的可用性。

    重新使用MySQL。MySQL的 InnoDB主键查询速度非常快。如果大部分缓存数据都可以放到VARCHAR字段中,那么主键查询的性能将更好。从memcached中按key查询几乎等价于

    MySQL的主键查询:将key 哈希到64-bit的整数,然后将数据存储到MySQL中。您可以把原始(不做哈希)的key存储都普通的字段中,然后建立二级索引来加快查询...key被动地失效,

    批量删除失效的key,等等。

    16、memcached是如何做身份验证的?

    没有身份认证机制!memcached是运行在应用下层的软件(身份验证应该是应用上层的职责)。memcached的客户端和服务器端之所以是轻量级的,部分原因就是完全没有实现身份验

    证机制。这样,memcached可以很快地创建新连接,服务器端也无需任何配置。如果您希望限制访问,您可以使用防火墙,或者让memcached监听unix domain socket。

    17、memcached的多线程是什么?如何使用它们?

    线程就是定律(threads rule)!在Steven Grimm和Facebook的努力下,memcached 1.2及更高版本拥有了多线程模式。多线程模式允许memcached能够充分利用多个CPU,并在

    CPU之间共享所有的缓存数据。memcached使用一种简单的锁机制来保证数据更新操作的互斥。相比在同一个物理机器上运行多个memcached实例,这种方式能够更有效地处理multi

    gets。如果系统的负载并不重,那么不需要启用多线程工作模式。如果您在运行一个拥有大规模硬件的、庞大的网站,将体验到看到多线程的好处。更多信息请参见:

    http://code.sixapart.com/svn/memcached/trunk/server/doc/threads.txt 。

    简单地总结一下:命令解析(memcached在这里花了大部分时间)可以运行在多线程模式下。memcached内部对数据的操作是基于很多全局锁的(因此这部分工作不是多线程的)。未

    来对多线程模式的改进,将移除大量的全局锁,提高memcached在负载极高的场景下的性能。

    18、memcached能接受的key的最大长度是多少?

    memcached能接受的key的最大长度是250个字符。需要注意的是,250是memcached服务器端内部的限制。如果使用的Memcached客户端支持"key的前缀"或类似特性,那么key

    (前缀+原始key)的最大长度是可以超过250个字符的。推荐使用较短的key,这样可以节省内存和带宽。

    19、memcached对item的过期时间有什么限制?

    item对象的过期时间最长可以达到30天。memcached把传入的过期时间(时间段)解释成时间点后,一旦到了这个时间点,memcached就把item置为失效状态,这是一个简单但

    obscure的机制。

    20、memcached最大能存储多大的单个item?

    memcached最大能存储1MB的单个item。如果需要被缓存的数据大于1MB,可以考虑在客户端压缩或拆分到多个key中。

    21、为什么单个item的大小被限制在1M byte之内?

    简单的回答:因为内存分配器的算法就是这样的。

    详细的回答:

    1)Memcached的内存存储引擎,使用slabs来管理内存。内存被分成大小不等的slabs chunks(先分成大小相等的slabs,然后每个slab被分成大小相等chunks,不同slab的chunk大小

    是不相等的)。chunk的大小依次从一个最小数开始,按某个因子增长,直到达到最大的可能值。如果最小值为400B,最大值是1MB,因子是1.20,各个slab的chunk的大小依次是:

    slab1 - 400B;slab2 - 480B;slab3 - 576B ...slab中chunk越大,它和前面的slab之间的间隙就越大。因此,最大值越大,内存利用率越低。Memcached必须为每个slab预先分配内

    存,因此如果设置了较小的因子和较大的最大值,会需要为Memcached提供更多的内存。

    2)不要尝试向memcached中存取很大的数据,例如把巨大的网页放到mencached中。因为将大数据load和unpack到内存中需要花费很长的时间,从而导致系统的性能反而不好。如果

    确实需要存储大于1MB的数据,可以修改slabs.c:POWER_BLOCK的值,然后重新编译memcached;或者使用低效的malloc/free。另外,可以使用数据库、MogileFS等方案代替

    Memcached系统。

    22、可以在不同的memcached节点上使用大小不等的缓存空间吗?如果这么做之后,memcached能够更有效地使用内存吗?

    Memcache客户端仅根据哈希算法来决定将某个key存储在哪个节点上,而不考虑节点的内存大小。因此,可以在不同的节点上使用大小不等的内存作为缓存空间。但是一般可以这样做

    :拥有较多内存的节点上可以运行多个memcached实例,每个实例使用的内存跟其他节点上的实例相同。

    23、什么是二进制协议,是否需要关注?

    二进制协议尝试为端提供一个更有效的、可靠的协议,减少客户端/服务器端因处理协议而产生的CPU时间。根据Facebook的测试,解析ASCII协议是memcached中消耗CPU时间最多的

    环节。

    24、memcached的内存分配器是如何工作的?为什么不适用malloc/free!?为何要使用slabs?

    实际上,这是一个编译时选项。默认会使用内部的slab分配器,而且确实应该使用内建的slab分配器。最早的时候,memcached只使用malloc/free来管理内存。然而,这种方式不能与

    OS的内存管理以前很好地工作。反复地malloc/free造成了内存碎片,OS最终花费大量的时间去查找连续的内存块来满足malloc的请求,而不是运行memcached进程。slab分配器就是

    为了解决这个问题而生的。内存被分配并划分成chunks,一直被重复使用。因为内存被划分成大小不等的slabs,如果item的大小与被选择存放它的slab不是很合适的话,就会浪费一些内存。

    25、memcached是原子的吗?

    所有的被发送到memcached的单个命令是完全原子的。如果您针对同一份数据同时发送了一个set命令和一个get命令,它们不会影响对方。它们将被串行化、先后执行。即使在多线程模

    式,所有的命令都是原子的。然是,命令序列不是原子的。如果首先通过get命令获取了一个item,修改了它,然后再把它set回memcached,系统不保证这个item没有被其他进程

    (process,未必是操作系统中的进程)操作过。memcached 1.2.5以及更高版本,提供了gets和cas命令,它们可以解决上面的问题。如果使用gets命令查询某个key的item,

    memcached会返回该item当前值的唯一标识。如果客户端程序覆写了这个item并想把它写回到memcached中,可以通过cas命令把那个唯一标识一起发送给memcached。如果该item

    存放在memcached中的唯一标识与您提供的一致,写操作将会成功。如果另一个进程在这期间也修改了这个item,那么该item存放在memcached中的唯一标识将会改变,写操作就会

    失败。

    性能和客户端库方面的问题

    26、memcached没有我的database快,为什么?

    在一对一比较中,memcached可能没有SQL查询快。但是,这不是memcached的设计目标。Memcached的目标是可伸缩性。当连接和请求增加的时候,memcached的性能将比

    大多数数据库查询好。可以先在高负载的环境(并发的连接和请求)中测试您的代码,然后再决定memcached是否适合您。

    27、使用不同的客户端库,可以访问到memcached中相同的数据吗?

    从技术上说,是可以的。但是可能会遇到下面三个问题:

    1)不同的库采用不同的方式序列化数据。举个例子,perl的Cache::Memcached使用Storable来序列化结构复杂的数据(比如hash references, objects, 等)。其他语言的客户端库很

    可能不能读取这种格式的数据。如果您要存储复杂的数据并且想被多种客户端库读取,那么您应该以简单的string格式来存储,并且这种格式可以被JSON、XML等外部库解析。

    2)从某个客户端来的数据被压缩了,从另一个客户端来的却没被压缩。

    3)各个客户端库可能使用不同的哈希算法(阶段一哈希)。在连接到多个memcached服务器端的情况下,客户端库根据自身实现的哈希算法把key映射到某台memcached上。正是因为

    不同的客户端库使用不同的哈希算法,所以被Perl客户端库映射到memcached A的key,可能又会被Python客户端库映射到memcached B,等等。Perl客户端库还允许为每台

    memcached指定不同的权重(weight),这也是导致这个问题的一个因素。

    28、什么是一致性哈希的客户端?

    这里有一篇文章很好地解释了它的用处:http://www.last.fm/user/RJ/journal/2007/04/10/392555 。

    客户端可以通过"前缀"来给key设置一个域(命名空间)。例如,在一个共享主机的环境中,可以将客户姓名作为"前缀",为key创建一个特定的域。在存储数据的时候,"前缀"可以用在

    key上,但是不应该参与哈希计算。目前,memcached自己还没有实现针对复杂结构数据的序列化方法,JSON则是一种被广泛使用的对象序列化格式。

    哈希 / 键分布

    29、什么时候失效的数据项会从缓存中删除?

    memcached 使用懒失效,当客户端请求数据项时, memcached 在返回数据前会检查失效时间来确定数据项是否已经失效。同样地,当添加一个新的数据项时,如果缓存已经满了, memcached 就会先替换失效的数据项,然后才是缓存中最少使用的数据项。

    命名空间

    30、memcached 不支持命名空间。以下提供几种模仿命名空间的方式:

    1)用键的前缀模仿命名空间:在真实的键之前加入有意义的前缀。

    2)用命名空间删除数据项:尽管 memcached 不支持使用任何类型的通配符或命名空间来完成删除操作,但是可以采用一些技巧来替代:

    在 PHP 中使用一个叫 foo 的命名空间:$ns_key = $memcache->get("foo_namespace_key");

    // if not set, initialize it

    if($ns_key=false) $memcache->set("foo_namespace_key", rand(1, 10000));

    $my_key = "foo_".$ns_key."_12345";

    清除命名空间:$memcache->increment("foo_namespace_key");

    应用设计

    31、在设计应用时,可以通过Memcached缓存那些内容?

    1)缓存简单的查询结果:查询缓存存储了给定查询语句对应的整个结果集,最合适缓存那些经常被用到,但不会改变的 SQL 语句对查询到的结果集,比如载入特定的过滤内容。

    $key = md5('SELECT * FROM rest_of_sql_statement_goes_here');

    if ($memcache->get($key)) {

          ` return $memcache->get($key);`

    }else {

        ` // Run the query and transform the result data into your final dataset form`

        ` $result = $query_results_mangled_into_most_likely_an_array`

         ` $memcache->set($key, $result, TRUE, 86400); // Store the result of the query for a day`

        ` return $result;`

    }

    记住,如果查询语句对应的结果集改变,该结果集不会展现出来。这种方法不总是有用,但它确实让工作变得比较快。

    2)缓存简单的基于行的查询结果:基于行的缓存会检查缓存数据key的列表,那些在缓存中的行可以直接被取出,不在缓存中的行将会从数据库中取出并以唯一的键为标识缓存起来,最

    后加入到最终的数据集中返回。随着时间的推移,大多数数据都会被缓存,这也意味着相比与数据库,查询语句会更多地从 memcached 中得到数据行。如果数据是相当静态的,我们可

    以设置一个较长的缓存时间。

    基于行的缓存模式对下面这种搜索情况特别有用:数据集本身很大或是数据集是从多张表中得到,而数据集取决于查询的输入参数但是查询的结果集之间的有重复部分。

    比如,如果你有用户 A , B , C , D , E 的数据集。你去点击一张显示用户 A , B , E 信息的页面。首先, memcached 得到 3 个不同的键,每个对应一个用户去缓存中查找,全部未

    命中。然后就到数据库中用 SQL 查询得到 3 个用户的数据行,并缓存他们。

    现在,你又去点击另一张显示显示 C , D , E 信息的页面。当你去查找 memcached 时, C , D 的数据并没有被命中,但我们命中了 E 的数据。然后从数据库得到 C , D 的行数据,缓

    存在 memcached 中。至此以后,无论这些用户信息怎样地排列组合,任何关于 A , B , C , D , E 信息的页面都可以从 memcached 得到数据了。

    3)缓存的不只是 SQL 数据,可以缓存最终完成的部分显示页面,以节省CPU计算时间

    例如正在制作一张显示用户信息的页面,你可能得到一段关于用户的信息(姓名,生日,家庭住址,简介),然后你可能会将 XML 格式的简介信息转化为 HTML 格式或做其他的一些工

    作。相比单独存储这些属性,你可能更愿意存储经过渲染的数据块。那时你就可以简单地取出被预处理后的 HTML 直接填充在页面中,这样节省了宝贵的 CPU 时间。

    32、使用分层的缓存

    memcached 可以高速处理大量的缓存数据,但是还是要根据系统的情况考虑维护多层的缓存结构。例如除了memcached缓存之外,还可以通过本地缓存(如ehcache、oscache等)建

    立起多级缓存。例如,可以采用本地缓存缓存一些基本数据,例如少量但访问频繁的数据(如产品分类,连接信息,服务器状态变量,应用配置变量等),缓存这些数据并让他们尽可能的

    接近处理器是有意义的 , 这样可以帮助减少生成页面的时间,并且在 memcached 失效的情况下可以增加可靠性。

    33、当数据更新时需要更新缓存

    用户编辑了自己的信息,当保存信息到数据库时,需要更新缓存中的数据或是简单地删除老的数据。如果马上更新数据,要防止从数据库读取那些刚刚更新过的数据。当用户习惯性地重新

    载入自己的用户信息来确认是否修改成功时,数据将从缓存中直接取出,这时他们获得了最新的数据。

    34、模拟带锁的添加命令

    如果你实在需要锁,你可以通过“添加”命令模仿锁的功能。尽管在未命中的情况下它不是那么有用,但如果你用它缓存平常的数据(应用服务器池的元数据)那还是有用的。

    比如,你要更新键 A 。

    1. 添加一个 "lock:A" 的键,这个键有一个持续几秒的过期时间(足够长以使你能完成计算和更新,也不要很长,因为如果锁进程挂了,这个键不会立即释放)

    2. 如果添加操作成功了,你就拥有了锁:从缓存获取键 A 的数据;利用客户端程序更改数据;更新缓存键 A 的数据;删除键 "lock:A" 。如果你不需要立即再次更新,就让它存活直到失效。

    3. 如果添加操作失败,说明有人获取了锁。这时让应用做些合适的事,比如返回老数据,等待后重试,或是其他的。

    以上这些操作类似 MySQL 将 GET_LOCK 的 timeout 值设置成 0 。没有办法在 memcached 中通过互斥锁模拟 GET_LOCK() 的 timeout 操作。

    35、预热你的缓存

    如果你有一个很高访问率的站点,并且你正想加入故障恢复功能或是其他全新的功能,你最终可能会碰到空缓存的问题。一开始缓存是空的,然后一大群人点击你的站点,在填充缓存的过

    程中,你的数据库可能会承受不住压力。为了解决这一问题,你可以试试任何可行的方法来 " 温暖 " 你的Memcached。方法:可以写一些脚本来缓存通用的页面;也可以写一个命令行工

    具来填充缓存。你可以在高峰时刻在缓存里填充一些内容。

    参考网页:

    http://shwangking-126-com.iteye.com/blog/284937

  • 相关阅读:
    Java基础回顾 —内部类
    JAVA设计模式之观察者模式
    Java基础回顾 —注解
    JAVA设计模式之代理模式
    Java基础回顾 —反射机制
    JAVA设计模式之策略模式
    Java基础回顾 —String常量池
    JAVA设计模式之单例模式
    Java基础回顾 —抽象类、接口、多态
    Git的简单应用
  • 原文地址:https://www.cnblogs.com/nazeebodan/p/3204159.html
Copyright © 2020-2023  润新知