0%

分布式锁可以基于以下几种方式实现:

  • 基于数据库的乐观锁,用于分布式锁
  • 基于缓存(Redis, memcached)实现分布式锁
  • 基于ZooKeeper实现分布式锁

在这篇文章中,主要讲讲ZooKeeper以及分布式锁的实现,通过了解基于ZooKeeper分布式锁实现的原理,我们会对ZooKeeper有一个基本的了解。

ZooKeeper介绍

首先谈谈ZooKeeper,ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务,它提供了一项基本服务:分布式锁服务。由于ZooKeeper的开源特性,后来我们的开发者在分布式锁的基础上,摸索了出了其他的使用方法:配置维护、组服务、分布式消息队列、分布式通知/协调等。

在ZooKeeper中,有一个被称为ZNode的节点,在该节点可以存储同步相关的数据,并且多个ZNode节点可以形成类似下图的结构。

image

基本命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 查看节点
ls /
2. 创建节点
create /zk myData
3. 查看节点
get /zk
4. 设置节点
set /zk myData2
5. 删除节点
delete /zk
6. 创建临时节点
create -e /han data
7. 创建顺序节点
create -s /han/ data
8. 创建顺序临时节点
create -s -e /han/ data

ZNode

客户端可以在一个ZNode上设置一个监视器(Watch),如果该ZNode数据发生变更,ZooKeeper会通知客户端,从而触发监视器中实现的逻辑的执行。其中ZNode有以下几种类型:

  • PERSISTENT
  • PERSISTENT_SEQUENTIAL
  • EPHEMERAL
  • EPHEMERAL_SEQUENTIAL

下面分别解释一下:

  1. PERSISTENT为持久节点,持久节点是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
    ZooKeeper命令:
1
create /zk myData
  1. PERSISTENT_SEQUENTIAL为持久顺序节点,基本特性与持久节点一致,但每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。命令:
1
create -s /han/ data

用这条命令的话,需要先创建/han节点,节点类型为PERSISTENT。

  1. EPHEMERAL为临时节点,客户端会话失效或连接关闭后,该节点会被自动删除,且不能在临时节点下面创建子节点,命令:
1
create -e /han

如果在临时节点下面还要创建子节点,那么zk就会提示:Ephemerals cannot have children

  1. EPHEMERAL_SEQUENTIAL为临时顺序节点,该节点的除了不是持久性节点,其他特性与持久顺序节点一致。命令:
1
create -s -e /han/ data
Read more »

认识RESTful API

RESTful API是目前比较成熟的API设计理论,它通过统一的API接口来对外提供服务,这样对其他调用者来说比较友好,更加容易实现前后端分离。那么如果要使用RESTful API来写我们的代码,那么就需要先知道RESTful API规范。

参考RESTful API规范

下面是两篇文章讲解RESTful API的,推荐:

  1. RESTful API 设计指南
  2. RESTful API 设计最佳实践

SpringMVC实现RESTful API

SpringMVC提供了一些注解来实现RESTful API, 例如@RestController,同时我们用Swagger来生成API文档,这样更加利于测试API。

常见swagger注解一览与使用

最常用的5个注解

@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiProperty:用对象接收参数时,描述对象的一个字段

其它若干

@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiClass
@ApiError
@ApiErrors
@ApiParamImplicit
@ApiParamsImplicit

其中@ApiOperation和@ApiParam参数说明

@ApiOperation和@ApiParam为添加的API相关注解,参数说明如下:
@ApiOperation(value = “接口说明”, httpMethod = “接口请求方式”, response = “接口返回参数类型”, notes = “接口发布说明”;其他参数可参考源码;
@ApiParam(required = “是否必须参数”, name = “参数名称”, value = “参数具体描述”

添加依赖

首先在pom.xml文件中添加swagger依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- swagger -->
<dependency>
<groupId>com.mangofactory</groupId>
<artifactId>swagger-springmvc</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.5.1</version>
</dependency>

Swagger-UI配置

首先从Swagger-UI下载地址下载Swagger-UI文件,然后将其拷贝到webapp目录下,我这里新建了一个swagger文件夹,然后解压后的文件拷贝到这个文件夹里面了。

修改swagger/index.html文件,默认是从连接http://petstore.swagger.io/v2/swagger.json获取 API 的JSON,这里需要将url值修改为http://{ip}:{port}/{projectName}/api-docs的形式,{}中的值根据自身情况填写。比如我的url值为:http://localhost:8080/lightblog/api-docs

编写swagger配置文件

配置完Swagger-UI后,我们需要配置Swagger,并将其交给Spring进行管理。

Read more »

利用接口回调实现JDBCTemplate

  1. 设计一个回调接口JDBCCallback, 用来设置参数和获取结果集, 代码如下:

    1
    2
    3
    4
    5
    6
    public interface JDBCCallback<T> {

    T rsToObject(ResultSet rs) throws SQLException;

    void setParams(PreparedStatement pstmt) throws SQLException;
    }
  2. 设计一个抽象类JDBCAbstractCallBack,该类实现JDBCCallback接口,重写接口中的两个方法,
    不需要具体实现,只需要重写一下就可以了,这样在DAO层用的时候不用这两个方法全部都要实现,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * JDBC abstract callback class, implements {@link JDBCCallback} interface.
    * @author Mingshan
    *
    * @param <T>
    */
    public abstract class JDBCAbstractCallBack<T> implements JDBCCallback<T> {

    @Override
    public T rsToObject(ResultSet rs) throws SQLException {
    return null;
    }

    @Override
    public void setParams(PreparedStatement pstmt) throws SQLException {
    // NOOP
    }
    }
  3. 设计一个JDBCTemplate类,该类实现增删改查的基本方法,把公共的代码抽取出来,以便DAO层去调用JDBCTemplate来实现具体的业务,部分代码如下:

    Read more »

在Hibernate框架中,一个实体类映射为一个数据库表,在进行多表查询时,如何将不同表中的数据整合起来,并且映射为一个实体类是利用Hibernate进行多表查询的关键,根据我的理解,先将代码整理一下:

实体类

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
@Entity
@Table(name = "ps_trends")
public class Trends implements Serializable {
private static final long serialVersionUID = -2228382525594394975L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@Column(name = "item_title")
private String itemTitle;

@Column(name = "item_content")
private String itemContent;

@Column(name = "type_id")
private int typeId;

@Column(name = "add_time")
private String addTime;

@Column(name = "view_count")
private int viewCount;

@Column(name = "is_image")
private int isImage;

@Column(name = "is_publish")
private int isPublish;

//临时属性
@Transient
private String itemTypeFlag;

@Transient
private String itemTypeName;

public Trends() {}

public Trends(int id, String itemTitle, String itemContent, String addTime, int viewCount,
String itemTypeName,String itemTypeFlag) {
super();
this.id = id;
this.itemTitle = itemTitle;
this.itemContent = itemContent;
this.addTime = addTime;
this.viewCount = viewCount;
this.itemTypeName = itemTypeName;
this.itemTypeFlag = itemTypeFlag;

}
setter ,getter方法
}

Read more »

Linux目录结构:

image

/: 根目录,一般根目录下只存放目录,不要存放文件,/etc、/bin、/dev、/lib、/sbin应该和根目录放置在一个分区中

/bin:/usr/bin: 可执行二进制文件的目录,如常用的命令ls、tar、mv、cat等。

/boot: 放置linux系统启动时用到的一些文件。/boot/vmlinuz为linux的内核文件,以及/boot/gurb。建议单独分区,分区大小100M即可

/dev:存放linux系统下的设备文件,访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱mount /dev/cdrom /mnt。

/etc:系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有/etc/inittab、/etc/fstab、/etc/init.d、/etc/X11、/etc/sysconfig、/etc/xinetd.d修改配置文件之前记得备份。
注:/etc/X11存放与x windows有关的设置。

/home:系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下,~表示当前用户的家目录,~test表示用户test的家目录。建议单独分区,并设置较大的磁盘空间,方便用户存放数据

/lib:/usr/lib:/usr/local/lib:系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助,比较重要的目录为/lib/modules。

Read more »

目录操作:

  • 创建目录:
1
mkdir $HOME/testFolder
  • 切换目录:

使用 cd 命令切换目录

1
cd $HOME/testFolder

使用 cd ../ 命令切换到上一级目录

1
cd ../
  • 移动目录

使用 mv 命令移动目录

1
mv $HOME/testFolder /var/tmp
  • 删除目录

使用 rm -rf 命令删除目录

1
rm -rf /var/tmp/testFolder
  • 查看目录下的文件

使用 ls 命令查看 /etc
目录下所有文件和文件夹

1
ls /etc
Read more »

在jdk1.5中引入枚举这个小功能,这个功能虽然用的不多,但是却给我们的开发带来很多便利,我们
今天来看看java的枚举是个什么样子。

枚举的主要操作方法

1
2
3
4
5
6
7
8
protected Enum(String name,int ordinal)  //接受枚举的名称和枚举的常量创建枚举对象  
protected final Object clone()throws CloneNotSupportedException //克隆枚举对象
public final int compareTo(E o) //比较枚举与指定对象的顺序
public final boolean equals(Object other) //比较两个枚举对象
public final int hashCode() //返回枚举常量的哈希码
public final String name() //返回枚举类的名称
public final int ordinal() //返回枚举常量的序号
public static <T extends Enum<T>> T valueOf(Class<T> enum Type,String name) //返回带指定名称的指定枚举类型的枚举常量

先定义一个枚举,用enum关键字

1
2
3
4
5
6
7
8
/**
* 定义枚举
* @author mingshan
*
*/
public enum EnumTest {
MON, TUE, WED, THU, FRI, SAT, SUN;
}

这里将星期定义为枚举类型,但没有赋值,既然已经定义好了,那么就先测试一下吧。

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
/**
* 枚举测试
* @author mingshan
*
*/
public class Test {

public static void main(String[] args) {

//遍历枚举
for(EnumTest e : EnumTest.values()) {
System.out.println(e.toString());
}

System.out.println("我是分割线------");


//switch 操作
EnumTest fri = EnumTest.FRI;

switch(fri){
case MON :
System.out.println("今天是星期一"); break;
case FRI :
System.out.println("今天是星期五"); break;
default :
System.out.println("-----"); break;
}

//返回
System.out.println(fri.getDeclaringClass());

//利用compareTo进行比较
switch (fri.compareTo(EnumTest.SAT)) {
case -1:
System.out.println("之前");
break;
case 1:
System.out.println("之后");
break;
default:
break;
}
}
}

我们可以遍历枚举,用java的foreach进行遍历,调用枚举的values方法获取定义的枚举列表,但当
我们编写自定义enum时,却不包含values这个方法,这个方法是当我门编译文件时,编译器自动帮我
们加上的。枚举还可以进行switch操作,可以对获取的枚举进行判断。利用compareTo函数进行比较两个
枚举的顺序

Read more »

在java多态中,引用与对象可以是不同的类型,如:

1
A b=new B();

运用多态时,引用类型可以是实际对象类型的父类,即实际对象类型已经是一个比较具体的类,而引用类型则是一个比较抽象的类,任何extends过声明引用类型的对象都可以赋值给这个引用变量,这样就可以做出类似动态数组的东西,如下:

1
2
3
4
5
6
Animal[] a=new Animal[2];
a[0]=new Dog();
a[1]=new Cat();
for(int i=0;i<a.length;i++){
a[i].eat();
}

a数组里面可以放任何Animal的子类对象,调用的时候可以把子类都当作Animal来操作,实际上调用的是子类的方法,是不是很好玩呢→_→

当然,多态的应用很广泛呢,参数和返回类型也可以多态,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Vet{
public void giveShot(Anmial a){

a.makeNoise();
}
}

class Pet{
public void a(){
Vet v=new Vet();
Dog dog=new Dog();
Cat cat=new Cat();
v.giveShot(dog);
v.giveShot(cat);
}

}

giveShot会接受任何Animal的子类的对象实例,根据传入的参数不同,会调用不同对象的方法。

在利用hibernate写通用DAO层时需要获取泛型的类型,比如我在写hql的update语句时需要获取泛型的实体类,由于泛型有擦除机制,所以与需要在运行过程中获取泛型的类型产生了矛盾。此时需要利用反射机制来实现此功能,下面来看一个小例子。

首先建一个实体类Dog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.Serializable;

public class Dog implements Serializable {
private static final long serialVersionUID = 8108340856807454651L;
private int age;
private String name;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

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

Read more »

最近在做一个简单的问答网站,首页的内容需要根据用户关注的话题和关注的问题等来展示最新的动态信息,刚开始的时候我想数据库表的设计是关键。

这里涉及到两个概念 推模式(push)拉模式(pull),这里有两篇大神分享的知识 新浪微博架构和FEED架构分析—人人架构paper0023新浪博客微博feed系统的推(push)模式和拉(pull)模式和时间分区拉模式架构探讨,讲解的比较清楚

简单来说,什么是推,什么是拉呢?

  1. 推模式是一个用户发表了一条动态,那么后台就遍历关注该用户的所有用户,向他们的feed中推送一条动态
  2. 拉模式与推模式相反,当用户刷新首页时,后台会遍历该用户关注的用户的动态信息,并将动态信息压入到该用户的feed中

简单介绍完推拉模式后,下面就要考虑数据库表该怎么设计了,我采用的是最简单的推模式,毕竟新手嘛,先掌握实现流程。


首先设计feed表,这里我设计一个feed表来存储推送的信息,该表主要有以下几个字段

  • id 自增id
  • suid 推送者uid
  • ruid 接收者uid
  • item_id 推送的信息id
  • type 推送信息类型
  • add_time 推送时间

这是我感觉很简单的feed表,毕竟我那个问答站推送类型不是太多,当然还需要为这个表设计索引哦。
设计完数据库表后,下面该考虑后台推送逻辑和代码如何实现以及前台首页如何渲染feed流信息。


后台我用的是PHP的ThinkPHP框架,新手表示该框架很好用,用该框架可以快速实现的我的想法,我感觉这一点还是很好的。首先在推送类型的选择中我选择了以下几种推送类型

  • 当一个话题下有新话题发起时,推送给关注该话题的用户
  • 当一个问题有回答时,推送给关注该问题的用户
  • 当一个话题的问题有新回答时,推送给关注该话题的用户

上面推送过程中会产生大量的重复信息,所以需要在推送时对推送信息进行过滤,以避免重复的推送信息出现。代码就是当上面的推送类型产生时,将信息写入到feed表中,这里并没有对推送用户进行筛选(对推送用户的筛选可以降低数据库的压力)。

前台渲染的话需要对信息进行排序整合,并对每一条动态信息进行标记,以便在模板渲染时匹配对应的模板。
我写的简单部分整合代码,需要对信息进行遍历整合(这里没写)

 //如果推送类型 为a  则代表推送信息类型为  用户关注的话题有关的问题或关注的问题产生的回答
 $aid=$val['item_id'];
 /**根据回答id获取获取与此回答有关的信息**/
 //获取推送人的uid
 $suid=$val['suid'];
 //获取当前用户对回答的赞同状态
 $upvote_status=$this->getUpvoteStatusByAid($aid, $uid);
//获取feed流 回答信息
 $a_info_all=$this->getFeedAnswerInfo($aid);
 $question_id=$a_info_all[0]['question_id'];
 //根据问题id获取与此问题相关的话题信息
 $tinfo_a=D('Topic')->getFeedTopicByQuestion($question_id);
 //将话题信息追加到回答信息数组中
 $a_info_all[0]['topic']= $tinfo_a;
 //将当前用户对回答的赞同状态最佳到信息数组中
 $a_info_all[0]['upvote_status']=$upvote_status;
 //将整理后的信息添加到feed数组中,并做一个标记 a,以便在模板中判断解析
 $arr_fd['answer']=$a_info_all;
 $arr_fd['feed_flag']='a';
 $feed_return_arr[]=  $arr_fd;

这里 $feed_return_arr[]是一个三维数组,在模板渲染的时候要注意一下。

Read more »