Servlet概念

  • Servlet就是一个接口,定义了Java类被浏览器访问到(tomcat识别)的规则。

  • 将来我们自定义一个类,实现Servlet接口,复写方法。

快速入门

  • 创建javaEE项目

  • 定义一个类,实现servlet接口

    1
    public class ServletDemo implements Servlet{}
  • 实现接口中的抽象方法

  • 配置servlet
    在web.xml中配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!--配置Servlet -->
    <servlet>
    <servlet-name>demo1</servlet-name>
    <servlet-class>cn.itcast.web.servlet.ServletDemo1</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>demo1</servlet-name>
    <url-pattern>/demo1</url-pattern>
    </servlet-mapping>

执行原理

  1. 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
  2. 查找web.xml文件,是否有对应的标签体内容。
  3. 如果有,则在找到对应的全类名
  4. tomcat会将字节码文件加载进内存,并且创建其对象
  5. 调用其方法

Servlet生命周期

1. 被创建:执行init方法,只执行一次
    * Servlet什么时候被创建?
        * 默认情况下,第一次被访问时,Servlet被创建
        * 可以配置执行Servlet的创建时机。
            * 在<servlet>标签下配置
                1. 第一次被访问时,创建
                    * <load-on-startup>的值为负数
                2. 在服务器启动时,创建
                    * <load-on-startup>的值为0或正整数
    * Servlet的init方法,只执行一次,说明一个Servlet在内存中只存在一个对象,Servlet是单例的
        * 多个用户同时访问时,可能存在线程安全问题。
        * 解决:尽量不要在Servlet中定义成员变量。即使定义了成员变量,也不要对修改值

2. 提供服务:执行service方法,执行多次
    * 每次访问Servlet时,Service方法都会被调用一次。
3. 被销毁:执行destroy方法,只执行一次
    * Servlet被销毁时执行。服务器关闭时,Servlet被销毁
    * 只有服务器正常关闭时,才会执行destroy方法。
    * destroy方法在Servlet被销毁之前执行,一般用于释放资源
  • Servlet3.0:

    • 好处:

      • 支持注解配置。可以不需要web.xml了。
    • 步骤:

      1. 创建JavaEE项目,选择Servlet的版本3.0以上,可以不创建web.xml
      2. 定义一个类,实现Servlet接口
      3. 复写方法
      4. 在类上使用@WebServlet注解,进行配置
        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
        @WebServlet("资源路径")
        @Target({ElementType.TYPE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface WebServlet {
        String name() default "";//相当于<Servlet-name>

        String[] value() default {};//代表urlPatterns()属性配置

        String[] urlPatterns() default {};//相当于<url-pattern>

        int loadOnStartup() default -1;//相当于<load-on-startup>

        WebInitParam[] initParams() default {};

        boolean asyncSupported() default false;

        String smallIcon() default "";

        String largeIcon() default "";

        String description() default "";

        String displayName() default "";
        }

如果redis要支撑10万+的并发,那应该怎么做?

  • 答案是:读写分离

单机的redis几乎不太可能说QPS超过10万+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂。
redis单机的瓶颈

读写分离,一般来说,对缓存,一般都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千,一两千大量的请求都是读,一秒钟二十万次读。

读写分离:主从架构 -> 读写分离 -> 支撑10万+读QPS的架构

主从读写分离实现的高并发

redis replication的核心机制

  • redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量

  • 一个master node是可以配置多个slave node的

  • slave node也可以连接其他的slave node

  • slave node做复制的时候,是不会block master node的正常工作的

  • slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了

  • slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量

  • slave,高可用性,有很大的关系

master持久化对于主从架构的安全保障的意义

如果采用了主从架构,那么建议必须开启master node的持久化!

不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了

master -> RDB和AOF都关闭了 -> 全部在内存中

master宕机,重启,是没有本地数据可以恢复的,然后就会直接认为自己IDE数据是空的

master就会将空的数据集同步到slave上去,所有slave的数据全部清空

100%的数据丢失

master节点,必须要使用持久化机制

第二个,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的

即使采用了后续讲解的高可用机制,slave node可以自动接管master node,但是也可能sentinal还没有检测到master failure,master node就自动重启了,还是可能导致上面的所有slave node数据清空故障

面试的回答方式

redis高并发:主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS。

redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最对只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。

redis高可用:如果你做主从架构部署,其实就是加上哨兵就可以了,就可以实现,任何一个实例宕机,自动会进行主备切换。

redis的过期策略有哪些?

  • 设置过期时间(redis会采用定期删除、惰性删除的策略)

  • 定期删除

所谓定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。

注意:

实际上redis是每隔100ms随机抽取一些key来检查和删除的,定期删除可能会导致很多过期key到了时间并没有被删除掉。这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。redis基本上就死了。

  • 惰性删除

在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下。

  • 通过上述两种手段结合起来,保证过期的key一定会被干掉。

  • 我的数据明明都过期了,怎么还占用着内存啊?

redis中的过期key靠定期删除没有被删除掉,导致大量占用内存,导致redis的内存装不下数据,此时redis会进行内存淘汰。

  • 我redis里写的数据怎么没了?

很简单你写的数据太多,内存满了,或者触发了什么条件,redis lru,自动给你清理掉了一些最近很少使用的数据。

redis的内存淘汰的一些策略?

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)。

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

手写一下LRU算法?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LRUCache<K, V> extends LinkedHashMap<K, V> {

private final int CACHE_SIZE;

// 这里就是传递进来最多能缓存多少数据
public LRUCache(int cacheSize) {
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
/*这块就是设置一个hashmap的初始大小,同时最后一个true指的是让
linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾*/
CACHE_SIZE = cacheSize;
}

@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 这个意思就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
return size() > CACHE_SIZE;
}

}

字符串类型(String)

  • 举例应用场景

商品编号,订单号采用string 的递增数字特性生成

  • redis的string 类型可以包含任意数据,包括图片等二进制或者序列化的对象等。单个value的值最大上限为1G字节。

  • 纯字符串操作命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> set key1 'hello redis' # 存值
    OK
    127.0.0.1:6379> get key1 # 取值
    "hello redis"
    127.0.0.1:6379> getset key1 redis # 将给定key1的值设为redis,并返回key1的旧值(old value)
    "hello redis"
    127.0.0.1:6379> get key1
    "redis"
    127.0.0.1:6379> del key1 # 删除 key1
    (integer) 1
  • 整数自增自减操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> set key2 10 # 存入一个值为10的整数字符串
OK
127.0.0.1:6379> incr key2 # 自增
(integer) 11
127.0.0.1:6379> incr key2 # 自增
(integer) 12
127.0.0.1:6379> incrby key2 5 # 自增指定数值 -- 5
(integer) 17
127.0.0.1:6379> decr key2 # 自减
(integer) 16
127.0.0.1:6379> decr key2 # 自减
(integer) 15
127.0.0.1:6379> decrby key2 5 # 自减指定数值 -- 5
(integer) 10
  • 其他操作命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> set key1 redis
OK
127.0.0.1:6379> append key1 hello # 将 hello 追加到 key1 原来的值的末尾,放回追加后字符串长度
(integer) 10
127.0.0.1:6379> get key1
"redishello"
127.0.0.1:6379> strlen key1 # 返回 key1 所储存的字符串值的长度
(integer) 10
127.0.0.1:6379> mset key1 v1 key2 v2 key3 v3 # 批量同时设置一个或多个 key-value 对
OK
127.0.0.1:6379> mget key1 key2 key3 # 返回所有(一个或多个)给定 key 的值
1) "v1"
2) "v2"
3) "v3"

散列类型(Hash)

  • 举例应用场景

保存大量的对象数据

  • redis hash介绍

hash 叫散列类型。等价于Java 中的 HashMap。但是在 redis 中 hash 的 key 必须是 string 类型。不支持其他类型。这个特性非常适合存储对象。因为一个对象可以有很多属性,存储起来就是键值对形式的。在 Reids 中,每个 Hash 可以存储多达 4 亿个键值对。

  • 相关操作命令
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
127.0.0.1:6379> hset user name zhangsan # 使用 hset 为 user 添加一个键值对 name = zhangsan
(integer) 1
127.0.0.1:6379> hset user age 18 # 使用 hset 为 user 添加一个键值对 age = 18
(integer) 1
127.0.0.1:6379> hget user name # 使用 hget 获取 user 中键为 name 的值
"zhangsan"
127.0.0.1:6379> hget user age # 使用 hget 获取 user 中键为 age 的值
"18"
127.0.0.1:6379> hgetall user # 使用 hgetall 获取 user 中所有的键值对
1) "name"
2) "zhangsan"
3) "age"
4) "18"
127.0.0.1:6379> hmset user name lisi age 20 # 使用 hmset 为 user 批量添加键值对
OK
127.0.0.1:6379> hmget user name age # 使用 hmget 批量获取 user 中键的值
1) "lisi"
2) "20"
127.0.0.1:6379> hdel user name# 使用 hdel 删除 user 一个(或多个)键值对
(integer) 1
127.0.0.1:6379> hexists user name # 使用 hexists 判断 user 中 name 元素是否存在
(integer) 0
127.0.0.1:6379> hexists user age # 使用 hexists user 中 age 元素是否存在
(integer) 1
127.0.0.1:6379> hkeys user # 使用 hkeys 只获得 user 中的字段名
1) "age"
127.0.0.1:6379> hvals user # 使用 hvals 只获得 user 中的字段值
1) "20"
127.0.0.1:6379> hlen user # 使用 hlen 获得 user 中字段(键值对)数量
(integer) 1

列表类型(List)

  • 举例应用场景

微博某个大v的粉丝就可以以list的格式放在redis里去缓存

key=某大v value=[zhangsan, lisi, wangwu]

商品,博客,文章下面的评论列表。

  • redis list介绍

在 Redis 中的 List 类型,其内部使用的是双向链表实现的,所以它具有双向链表具有的相关特性。其常用操作就是向列表两端添加或删除元素。这使得 List 既可以当做栈(先进后出)来使用,也可以当做队列(先进先出)来使用。

  • 相关操作命令
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
127.0.0.1:6379> lpush list 1 2 3 4 # 使用 lpush 将 1 2 3 4 依次插入到 list 的左端
(integer) 4
127.0.0.1:6379> rpush list 5 6 7 8 # 使用 rpush 将 5 6 7 8 依次插入到 list 的右端
(integer) 8
127.0.0.1:6379> lrange list 0 -1 # 使用 lrange 获取 指定区间上所有值(0 -1 表示获取全部)
1) "4"
2) "3"
3) "2"
4) "1"
5) "5"
6) "6"
7) "7"
8) "8"
127.0.0.1:6379> lpop list # 使用 lpop 弹出 list 左端的一个值,并返回弹出的值
"4"
127.0.0.1:6379> lpop list
"3"
127.0.0.1:6379> rpop list # 使用 rpop 弹出 list 右端的一个值,并返回弹出的值
"8"
127.0.0.1:6379> rpop list
"7"
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "1"
3) "5"
4) "6"
127.0.0.1:6379> llen list # 使用 llen 获取 list 中元素个数
(integer) 4

集合类型(Set)

  • 举例应用场景

可以基于set玩儿交集、并集、差集的操作

可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁

  • 相关操作命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> sadd set a b c 1 2 3  # 使用 sadd 将 a b c 1 2 3 添加到 set 集合中
(integer) 6
127.0.0.1:6379> sadd set a b 2 # 添加重复元素,返回成功添加 0 个,说明 set 中元素不重复
(integer) 0
127.0.0.1:6379> srem set a b 1 # 使用 srem 删除 set 集合中的 a b 1 三个元素
(integer) 3
127.0.0.1:6379> smembers set # 使用 smembers 获取 set 集合中所以元素
1) "2"
2) "c"
3) "3"
127.0.0.1:6379> sismember set a # 使用 sismember 判断 a 是否在 set 集合中
(integer) 0
127.0.0.1:6379> sismember set c # 使用 sismember 判断 c 是否在 set 集合中
(integer) 1
  • 集合的并集运算 A ∪ B
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> sadd seta 1 2 3
(integer) 3
127.0.0.1:6379> sadd setb 3 4 5
(integer) 3
127.0.0.1:6379> sunion seta setb # 使用 sunion 计算 seta 和 setb 的并集
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
  • 集合的交集运算 A ∩ B
1
2
3
4
5
6
127.0.0.1:6379> sadd seta 1 2 3
(integer) 3
127.0.0.1:6379> sadd setb 3 4 5
(integer) 3
127.0.0.1:6379> sinter seta setb # 使用 sinter 计算 seta 和 setb 的交集
1) "3"
  • 集合的差集运算 A - B
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> sadd seta 1 2 3
(integer) 3
127.0.0.1:6379> sadd setb 3 4 5
(integer) 3
127.0.0.1:6379> sdiff seta setb # 使用 sdiff 计算 seta - setb (属于seta 但不属于 setb)
1) "1"
2) "2"
127.0.0.1:6379> sdiff setb seta # 使用 sdiff 计算 setb - setb (属于setb 但不属于 seta)
1) "4"
2) "5"

有序集合类型 (sorted set)

  • 举例应用场景

商品销售,软件下载等各种排行榜

还可以分页查询

  • 相关操作命令
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
127.0.0.1:6379> zadd board 81 java 75 python 90 c++ # 使用 zadd 增加一到多个value/score对,score 放在前面
(integer) 3
127.0.0.1:6379> zscore board java # 使用 zscore 获取 java 的 score
"81"
127.0.0.1:6379> zrange board 0 -1 # 使用 zrange 获取指定区间(0 -1 表示全部)上的降序排名
1) "python"
2) "java"
3) "c++"
127.0.0.1:6379> zrange board 0 -1 withscores # 带上 winthscores 可以一并获取元素的 score
1) "python"
2) "75"
3) "java"
4) "81"
5) "c++"
6) "90"
127.0.0.1:6379> zrevrange board 0 -1 withscores # 使用 zrevrange 获取指定区间(0 -1 表示全部)上的升序排名
1) "c++"
2) "90"
3) "java"
4) "81"
5) "python"
6) "75"
127.0.0.1:6379> zrangebyscore board -inf +inf withscores # 使用 zrangebyscore 获取 负无穷(-inf)到 正无穷(+inf)区间上所以元素的降序排名
1) "python"
2) "75"
3) "java"
4) "81"
5) "c++"
6) "90"
127.0.0.1:6379> zrevrangebyscore board +inf -inf withscores # 使用 zrevrangebyscore 获取正无穷(+inf)到 负无穷(-inf)区间上所以元素的升序排名
1) "c++"
2) "90"
3) "java"
4) "81"
5) "python"
6) "75"
127.0.0.1:6379> zcard board # 使用 zcard 计算 board 集合的元素个数
(integer) 3
127.0.0.1:6379> zrem board java python # 使用 zrem 删除 board 集合中的一个或多个元素
(integer) 2

redis与memcached有什么区别?

  • redis的数据类型比memcached的多,支持的数据结构更为丰富。

  • redis支持集群,而memcached不支持集群。

redis的线程模型是什么?

文件事件处理器

redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器,是单线程的,所以redis才叫做单线程的模型。
下图框起来的整个部分指的是文件事件处理器:
redis单线程模型

为什么单线程的redis比多线程的memcached效率高(单线程还支持高并发 )?

  • redis的各个时间处理器属于纯内存操作,效率高

  • 核心是基于非阻塞的IO多路复用机制

  • 单线程反而避免了多线程的频繁上下文切换问题

项目中的缓存是怎么使用的?

经典的电商项目都喜欢将导航栏,菜单栏,三级分类菜单数据放到redis缓存中,因为这些数据往往在用户访问的时候都能访问到,如果大量用户都去访问数据库会造成数据库访问压力,甚至宕机,而缓存基于内存,天生就支持高性能高并发,所以我们会将这些用户经常能访问到的数据都放到redis缓存中。

为什么要使用缓存?

一般情况下我们的项目里面基本上不会有高并发的场景,有些复杂查询的场景需要将数据放到缓存中,后续会大幅度提升访问性能,提升用户体验。

用了缓存会有什么不良的后果?

  • 缓存与数据库双写不一致

数据库里面的数据更新过后不及时更新到缓存会导致用户获取到缓存中的数据与数据库中不同步。

  • 缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大

  • 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据。这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

  • 缓存雪崩

缓存雪崩是指缓存数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

  • 缓存并发竞争

线程死锁的原理

当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
线程死锁的原理

线程死锁的代码实现

唯一对象锁A

1
2
3
4
public class LockA {
private LockA(){}
public static final LockA locka = new LockA();
}

唯一对象锁B

1
2
3
4
public class LockB {
private LockB(){}
public static final LockB lockb = new LockB();
}

死锁代码具体实现

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 class DeadLock implements Runnable{
private int i = 0;
public void run(){
while(true){
if(i%2==0){
//先进入A同步,再进入B同步
synchronized(LockA.locka){
System.out.println("if...locka");
synchronized(LockB.lockb){
System.out.println("if...lockb");
}
}
}else{
//先进入B同步,再进入A同步
synchronized(LockB.lockb){
System.out.println("else...lockb");
synchronized(LockA.locka){
System.out.println("else...locka");
}
}
}
i++;
}
}
}

测试代码

1
2
3
4
5
6
7
8
9
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock dead = new DeadLock();
Thread t0 = new Thread(dead);
Thread t1 = new Thread(dead);
t0.start();
t1.start();
}
}

饿汉式

1
2
3
4
5
6
7
8
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}

这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。 这种方式基于类加载机制避免了多线程的同步问题,但是有其他的静态方法导致类装载,这时候初始化instance显然没有达到懒加载的效果。

懒汉模式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作。

懒汉模式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。

双重检查模式 (DCL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private volatile static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return singleton;
}
}

这种写法在getSingleton方法中对singleton进行了两次判空,第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。在这里用到了volatile关键字,双重检查模式是正确使用volatile关键字的场景之一。
在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。 DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。

静态内部类单例模式(建议使用)

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}

第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder并初始化sInstance,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

枚举单例

1
2
3
4
5
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。

单例模式避免反序列化重新创建对象

在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法

1
2
3
private Object readResolve() throws ObjectStreamException{
return singleton;
}

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

使用容器实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;
}
}
public static ObjectgetService(String key) {
return objMap.get(key) ;
}
}

用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

数据库自增ID

这个就是说你的系统里每次得到一个id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个id。拿到这个id之后再往对应的分库分表里去写入。

这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成递增几个id之后的一个值;但是无论怎么说都是基于单个数据库。

适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

并发很低,几百/s,但是数据量大,几十亿的数据,所以需要靠分库分表来存放海量的数据。
数据库自增原理分析

UUID

好处就是本地生成,不要基于数据库来了;不好之处就是,uuid太长了,作为主键性能太差了,不适合用于主键。

适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。

UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

获取系统当前时间

这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,订单编号,时间戳 + 用户id + 业务含义编码。

snowflake算法

twitter开源的分布式id生成算法,就是把一个64位的long型的id,1个bit是不用的,用其中的41 bit作为毫秒数,用10 bit作为工作机器id,12 bit作为序列号

1 bit:不用,为啥呢?因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0

41 bit:表示的是时间戳,单位是毫秒。41 bit可以表示的数字多达2^41 - 1,也就是可以标识2 ^ 41 - 1个毫秒值,换算成年就是表示69年的时间。

10 bit:记录工作机器id,代表的是这个服务最多可以部署在2^10台机器上哪,也就是1024台机器。但是10 bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表2 ^ 5个机房(32个机房),每个机房里可以代表2 ^ 5个机器(32台机器)。

12 bit:这个是用来记录同一个毫秒内产生的不同id,12 bit可以代表的最大正整数是2 ^ 12 - 1 = 4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id

64位的long型的id,64位的long -> 二进制

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

2018-01-01 10:00:00 -> 做了一些计算,再换算成一个二进制,41bit来放 -> 0001100 10100010 10111110 10001001 01011100 00

机房id,17 -> 换算成一个二进制 -> 10001

机器id,25 -> 换算成一个二进制 -> 11001

snowflake算法服务,会判断一下,当前这个请求是否是,机房17的机器25,在2175/11/7 12:12:14时间点发送过来的第一个请求,如果是第一个请求

假设,在2175/11/7 12:12:14时间里,机房17的机器25,发送了第二条消息,snowflake算法服务,会发现说机房17的机器25,在2175/11/7 12:12:14时间里,在这一毫秒,之前已经生成过一个id了,此时如果你同一个机房,同一个机器,在同一个毫秒内,再次要求生成一个id,此时我只能把加1

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000001

比如我们来观察上面的那个,就是一个典型的二进制的64位的id,换算成10进制就是910499571847892992。

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
* <p>名称:IdWorker.java</p>
* <p>描述:分布式自增长ID</p>
* <pre>
* Twitter的 Snowflake JAVA实现方案
* </pre>
* 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
* 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
* 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
* 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
* 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
* 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
* 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
* <p>
* 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
*
* @author Polim
*/
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;

private final long workerId;
// 数据标识id部分
private final long datacenterId;

public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}

if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;

return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}
}

1. Markdown是什么

Markdown是一种轻量级标记语言,它以纯文本形式(易读、易写、易更改)编写文档,并最终以HTML格式发布。
Markdown也可以理解为将以MARKDOWN语法编写的语言转换成HTML内容的工具。

2. 创造了它?

它由Aaron SwartzJohn Gruber共同设计,Aaron Swartz就是那位于去年(2013年1月11日)自杀,有着开挂一般人生经历的程序员。维基百科对他的介绍是:软件工程师、作家、政治组织者、互联网活动家、维基百科人

他有着足以让你跪拜的人生经历:

  • 14岁参与RSS 1.0规格标准的制订。
  • 2004年入读斯坦福,之后退学。
  • 2005年创建Infogami,之后与Reddit合并成为其合伙人。
  • 2010年创立求进会(Demand Progress),积极参与禁止网络盗版法案(SOPA)活动,最终该提案被撤回。
  • 2011年7月19日,因被控从MIT和JSTOR下载480万篇学术论文并以免费形式上传于网络被捕。
  • 2013年1月自杀身亡。

Aaron Swartz

天才都有早逝的归途。

3. 为什么要使用它?

  • 它是易读(看起来舒服)、易写(语法简单)、易更改纯文本。处处体现着极简主义的影子。
  • 兼容HTML,可以转换为HTML格式发布。
  • 跨平台使用。
  • 越来越多的网站支持Markdown。
  • 更方便清晰地组织你的电子邮件。(Markdown-here, Airmail)
  • 摆脱Word(我不是认真的)。

4. 怎么使用?

如果不算扩展,Markdown的语法绝对简单到让你爱不释手。

Markdown语法主要分为如下几大部分:
标题段落区块引用代码区块强调列表分割线链接图片反斜杠 \符号’`’

4.1 标题

两种形式:
1)使用=-标记一级和二级标题。

一级标题
=========
二级标题
---------

效果:

一级标题

二级标题

2)使用#,可表示1-6级标题。

# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题

效果:

一级标题

二级标题

三级标题

四级标题

五级标题
六级标题

4.2 段落

段落的前后要有空行,所谓的空行是指没有文字内容。若想在段内强制换行的方式是使用两个以上空格加上回车(引用中换行省略回车)。

4.3 区块引用

在段落的每行或者只在第一行使用符号>,还可使用多个嵌套引用,如:

> 区块引用
>> 嵌套引用

效果:

区块引用

嵌套引用

4.4 代码区块

代码区块的建立是在每行加上4个空格或者一个制表符(如同写代码一样)。如
普通段落:

void main()
{
printf(“Hello, Markdown.”);
}

代码区块:

void main()
{
    printf("Hello, Markdown.");
}

注意:需要和普通段落之间存在空行。

4.5 强调

在强调内容两侧分别加上*或者_,如:

*斜体*,_斜体_
**粗体**,__粗体__

效果:

斜体斜体
粗体粗体

4.6 列表

使用·+、或-标记无序列表,如:

-(+*) 第一项
-(+*) 第二项
- (+*)第三项

注意:标记后面最少有一个_空格制表符_。若不在引用区块中,必须和前方段落之间存在空行。

效果:

  • 第一项
  • 第二项
  • 第三项

有序列表的标记方式是将上述的符号换成数字,并辅以.,如:

1 . 第一项
2 . 第二项
3 . 第三项

效果:

  1. 第一项
  2. 第二项
  3. 第三项

4.7 分割线

分割线最常使用就是三个或以上*,还可以使用-_

4.8 链接

链接可以由两种形式生成:行内式参考式
行内式

[younghz的Markdown库](https:://github.com/younghz/Markdown “Markdown”)。

效果:

younghz的Markdown库

参考式

[younghz的Markdown库1][1]
[younghz的Markdown库2][2]
[1]:https:://github.com/younghz/Markdown “Markdown”
[2]:https:://github.com/younghz/Markdown “Markdown”

效果:

younghz的Markdown库1
younghz的Markdown库2

注意:上述的[1]:https:://github.com/younghz/Markdown "Markdown"不出现在区块中。

4.9 图片

添加图片的形式和链接相似,只需在链接的基础上前方加一个

4.10 反斜杠\

相当于反转义作用。使符号成为普通符号。

4.11 符号’`’

起到标记作用。如:

`ctrl+a`

效果:

ctrl+a

5. 在用?

Markdown的使用者:

  • GitHub
  • 简书
  • Stack Overflow
  • Apollo
  • Moodle
  • Reddit
  • 等等

6. 尝试一下

  • Chrome下的插件诸如stackeditmarkdown-here等非常方便,也不用担心平台受限。
  • 在线的dillinger.io评价也不错
  • Windowns下的MarkdownPad也用过,不过免费版的体验不是很好。
  • Mac下的Mou是国人贡献的,口碑很好。
  • Linux下的ReText不错。

当然,最终境界永远都是笔下是语法,心中格式化 :)。


注意:不同的Markdown解释器或工具对相应语法(扩展语法)的解释效果不尽相同,具体可参见工具的使用说明。
虽然有人想出面搞一个所谓的标准化的Markdown,[没想到还惹怒了健在的创始人John Gruber]
(http://blog.codinghorror.com/standard-markdown-is-now-common-markdown/ )。


以上基本是所有traditonal markdown的语法。