这篇文章也是我对分布式锁认知的一个总结吧,目前所做的流量工程使用到了基于Redis的分布式锁,由此延伸到其他的概念及应用

什么是分布式锁

提到锁,Java开发者的第一印象肯定会想到Java线程中的锁,基于Java的内存模型:每个线程有自己的内存空间,同时这些线程有个共享的主内存,不同线程先更新自身内存再更新主内存,在更新主内存的同一条数据的时候,需要加锁控制。多线程开发中常见锁的关键字有synchronized、ReentrantLock等,但多线程锁是存在一个JVM之中的,如果操作的数据不在一个JVM中,多线程中锁就失效了,这种情况下分布式锁就诞生了,即多个Java实例、甚至不一定是Java程序、或多个系统需要操作同一个副本数据的时候,需要一个指挥交通的人指定操作的先后顺序,这就是分布式锁的概念。

什么场景下需要分布式锁

单纯将分布式锁的概念可能有点抽象,以流量业务场景中例子来说明,手机用户可以在手机App端、网上营业厅、wap手厅进行流量业务的操作,如果发现该用户没有流量账户的时候,会首先给该用户创建一个专门的流量账户,如果用户在app端、网上营业厅端同时操作的时候,可能会给该用户创建2个账户;再举一个例子,一个公共集团账户,下面包含很多账户,给下面账户充值的时候,会对该集团账本进行资金扣减,高并发多请求的时候会到导致并发失败,这时候为了减少失败率,提升QPS/TPS,同样需要分布式锁

行业内解决方案

上面说到了扣减同一账本的时候,估计就可以想到,其实这可以用乐观锁悲观锁来实现,确实,广义上来讲,乐观锁悲观锁也是分布式锁,这是一种基于数据库实现的分布式锁,另外还有其他多种方案,比如基于ZooKeeper实现分布式锁、基于Redis实现分布式锁(我们项目中就是使用该方式),下面一个一个分析:

  • 1、基于数据库悲观锁乐观锁
    a)悲观锁:每次拿到数据的时候都认为数据是被人改过的或有可能被更改,因为每次都进行加锁,最直接的方式:
    1
    select * from table where (...) for update

b)乐观锁:每次去拿数据的时候都认为别人不会修改,但是更新的时候会判断有没有其他操作更新了该数据,根据比较版本号(或时间戳)的方式来衡量当前版本是不是最新版本:

1
2
select version from table
update table set version=version+1 where version=?

  • 2、基于Redis实现
    Redis版本的实现也是因为不管单机还是集群分布式,都需要从同一个地方获取锁,而单线程的Redis恰巧具备这样的特性,再加上合理的命令,自然可以实现分布式锁的功能。当然,即使使用Redis实现,也有不同的实现逻辑,下面依次介绍:
    a)set+expire(细节待补充)
    b)setnx+expire
    c)setnx+get+getset

  • 3、基于ZooKeeper实现
    zookeeper分布式锁是基于zk的临时有序节点的特性实现的,其主要思想为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的临时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题

总结

基本把我对分布式锁的了解大概讲了一遍。可以实现分布式锁功能的,包括数据库、nosql、分布式协调服务等等。根据业务的场景、现状以及已经依赖的服务,应用可以使用不同分布式锁实现。

又到了年终做总结的日子,过去一年,通过项目落地与开源资料学习相结合,认识到作为一个开发人员一个技术人员保持竞争力的重要性和核心要素。

年初的几个月,系统服务化的节奏依然属于初期,账务中心属于整个浙江移动服务化比较底层核心的位置,因为主要涉及用户的各种资金资产,所以重构服务化步伐比较缓慢,除了不停的梳理代码、写文档之外,就是要熟悉基础设施架构的使用与落地; 下半年,正式进入开发周期,项目上线也安排进日程,也就是最近半年,深入了解了SOA服务体系或微服务体系的整体架构实现、技术细节、各个点存在的坑。
主要包括:

  • 1、事务一致性的实现,包括分布式事务的强一致性与最终一致性
  • 2、性能调优,从QPS、负载到架构、代码、数据库的优化
  • 3、Java虚拟机的组成、参数、oom等监控与调试
  • 4、运营商业务支撑系统(crm、boss)模型建立、业务流程、整体架构等应用
  • 5、自己学习研究jdk的源码及一些开源框架

当然,以上每一个点专门拿出来都可以详细深入到每一个概念及原理,一年的学习不足以深入覆盖的每一个细节,因此仍在不停学习中。

“我们读书越多,越发现我们是无知的”,这句话在过去一年深有体会,12年毕业,至今已有4年半的工作经历,从刚开始自己学习一个框架,都觉得自己又进步了一分,到如今对每一个新技术都战战兢兢保持敬畏之心,自己的心态也在发生改变,作为一个软件行业的开发人、一个技术人,在如今快节奏的互联网更新迭代速度下,必须实时更新自己的技术树。

  • 一、始终保持学习的积极性;
  • 二、利用看书+利用网络资源覆盖技能知识点;
  • 三、通过专业技术网站、垂直社区了解行业趋势;
  • 四、将自己学习到的技能点与实际项目相结合,将知识落地;
  • 五、与他人交流,同行业的最佳,了解其他企业其他开发人员的技术实现及关注点。

2017,come on!

CAP理论

说到分布式事务,必须提到的就是CAP理论,即同时满足“CAP定律”中的“一致性”、“可用性”和“分区容错性”三者是不可能的;大多数的场景,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可;阿里云好像有一个GTS分布式事务框架,是做强一致性的,目前行业还未看到有做强一致性方案。

两阶段提交

提到分布式系统,必然要提到分布式事务。要想理解分布式事务,不得不先介绍一下两阶段提交协议。拿网上的一个例子来说明:
第一阶段,张老师作为“协调者”,给小强和小明(参与者、节点)发微信,组织他们俩明天8点在学校门口集合,一起去爬山,然后开始等待小强和小明答复。
第二阶段,如果小强和小明都回答没问题,那么大家如约而至。如果小强或者小明其中一人回答说“明天没空,不行”,那么张老师会立即通知小强和小明“爬山活动取消”。
这时候会发现,这个过程中可能有很多问题的。如果小强没看手机,那么张老师会一直等着答复,小明可能在家里把爬山装备都准备好了却一直等着张老师确认信息。更严重的是,如果到明天8点小强还没有答复,那么就算“超时”了,那小明到底去还是不去集合爬山呢?这就是两阶段提交协议的弊病,所以后来业界又引入了三阶段提交协议来解决该类问题,当然这不是两阶段提价的最重要问题,最重要的两阶段有个预提交的阶段,非常影响性能,这在讲究用户体验至上的情况下,基本不能容忍的。

最终一致性解决方案

既然不能容忍强一致性带来的性能问题,那肯定是要最终一致性来保证事务和数据的一致性了,而且这也是目前行业内比较流行的解决方案,但由于各行各业业务千差万别,所以目前仍然没有一个万能方案来适应各个公司各个业务场景,我看过支付宝、蘑菇街、乃至其他同行分享的方案,都有公司很深的烙印。最终一致性的原理,即是规避开分布式事务的强一致性,在可能出现不一致场景的情况下,允许出现一段时间的数据不一致,但经过处理,最终系统整体的数据是准确的是符合一致性的。实现这种最终一致性方案,有基于补偿、基于消息的、基于tcc的,但万变不离其宗,基本都是对rpc调用过程进行结果确认,再通过反向处理服务保证最后数据的准确性。

亚信落地方案

作为服务运营商的解决方案(运营商解决方案一般以省为单位,该方案为浙江移动),就以手机运营商这边很重要的一个场景为例,用户有1000积分,可以兑换100M流量,而扣减用户1000积分和给用户送100M流量分属2个系统(和实际情况不完全一致,仅举例说明),需要调用2次rpc服务,一旦扣减积分成功,但兑换流量因为业务或者超时等业务或系统异常,流量这一步的调用失败,而这时候1000积分已经扣减掉了,系统发现这个问题,则需要将1000积分重新给用户加上,这部分功能由拦截器发现集成到客户端的jar里面,然后对调用情况进行落表,记录下调用2次rpc过程的成功和失败,出现不一致的时候,发送消息给积分系统,由积分系统来将扣减的1000积分重新加上,流程见下图(小陶提供):

总结

分布式事务的解决方案必须根据业务的实际情况来考虑,同时衍生出一系列问题,比如消息的丢失、重复,反撤的性能如何等等,都是需要考虑的问题,里面的内容仍需要不断完善。

最近项目中遇到一个问题,数据在不同库之间进行同步的时候,部分数据没有同步过去;

经过检查,发现是同步的时候很不明智的用序列sequence检查数据的先后性,然后增量数据同步,因为依次取数据库sequence的时候,应用程序中的数据入库持久化是有先后顺序的,优先取sequence的应用程序可能执行比较慢,后取sequence的应用程序已经提前完成持久化动作,因此sequence生成顺序和数据持久化入库顺序并不保持一致;

Oracle是如此,mysql呢?mysql设置自增主键是否也是这种情况,于是做了个测试

在mysql建一张表,设置自增主键

1
2
3
4
CREATE TABLE TEST_ZC (   
ID NUMBER(19,0) NOT NULL AUTO_INCREMENT,
TIME TIMESTAMP NULL DEFAULT NULL,
);

然后应用程序,比较主键和时间


可有看到时间和序列之间并不一致,即Oracle中sequence和MySQL自增主键一样,和数据持久化入库时间并不具有一致性。

最近进行程序测试的时候报了一个异常,发现是list使用remove的是报的错位,信息如下:

1
2
java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(Unknown Source)

list为什么使用remove会报这个错呢?查看原因,发现是因为list是使用数组转换List的Arrays.asList方法得到的,查看一下Java API对此方法的描述

1
2
3
4
5
6
7
8
9
10
11
12
asList
public static <T> List<T> asList(T... a)返回一个受指定数组支持的固定大小的列表。(对返回列表的更改会“直接写”到数组。)
此方法同 Collection.toArray() 一起,充当了基于数组的 API 与基于 collection 的 API 之间的桥梁。
返回的列表是可序列化的,并且实现了 RandomAccess。
此方法还提供了一个创建固定长度的列表的便捷方法,该列表被初始化为包含多个元素:

List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

参数:
a - 支持列表的数组。
返回:
指定数组的列表视图。

只有该方法的定义,并未解释异常的原因,那就查一下源码,源码如下:

1
2
3
4
public static transient List asList(Object aobj[])
{
return new ArrayList(aobj);
}

返回值是一个ArrayList

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
public class Arrays
{
private static class ArrayList extends AbstractList
implements RandomAccess, Serializable
{

public int size()
{
return a.length;
}

public Object[] toArray()
{
return (Object[])((Object []) (a)).clone();
}

public Object[] toArray(Object aobj[])
{
int i = size();
if(aobj.length < i)
return Arrays.copyOf(a, i, ((Object) (aobj)).getClass());
System.arraycopy(((Object) (a)), 0, ((Object) (aobj)), 0, i);
if(aobj.length > i)
aobj[i] = null;
return aobj;
}

public Object get(int i)
{
return a[i];
}

public Object set(int i, Object obj)
{
Object obj1 = a[i];
a[i] = obj;
return obj1;
}

public int indexOf(Object obj)
{
if(obj == null)
{
for(int i = 0; i < a.length; i++)
if(a[i] == null)
return i;

} else
{
for(int j = 0; j < a.length; j++)
if(obj.equals(a[j]))
return j;

}
return -1;
}

public boolean contains(Object obj)
{
return indexOf(obj) != -1;
}

private static final long serialVersionUID = -2764017481108945198L;
private final Object a[];

ArrayList(Object aobj[])
{
if(aobj == null)
{
throw new NullPointerException();
} else
{
a = aobj;
return;
}
}
}

ArrayList是Array中的一个内部类,其中并没有remove方法

1
2
3
4
public Object remove(int i)
{
throw new UnsupportedOperationException();
}

查看父类AbstractList,remove方法在此,它直接抛出了该异常;
至此我们知道,数据转换List得到的不是java.util下面顶级list,而是数据内部一个继承AbstractList的list,而这个种类的list并不具备remove方法。

想了一下,解决办法如下:
重新new一个list,将转换得到的list值转入进去

1
List newList = new ArrayList<>(arrayTolist);

这样就可以继续使用remove方法了。

公司做企业级服务架构,而微服务是现今互联网很火的一个架构模式。两者既有相似的地方又有不同的特点,根据自己理解写一个对两者的看法。

SOA:

面向服务架构,java级企业开发的首选。J2EE在统一开发中有着很好的使用,但是随着模块功能的不断扩展或者变动,对于其中一点的维护可能会影响到整体的项目。
所以有了微服务,彻底的将耦合性再次的降低了。为什么要讲我实习呢,因为微服务的使用会随着单一进程的传统应用被拆分为一系列的多进程服务后,意味着开发、调试、测试、集成、监控和发布的复杂度都会相应增大。 必须要有合适的自动化基础设施来支持微服务架构模式,否则开发、运维成本将大大增加。所以自动化运维是十分必要的。

微服务:

采用一组服务的方式来构建一个应用,服务独立部署在不同的进程中,不同服务通过一些轻量级交互机制来通信,例如 RPC、HTTP 等,服务可独立扩展伸缩,每个服务定义了明确的边界,不同的服务甚至可以采用不同的编程语言来实现,由独立的团队来维护。简单的来说,一个系统的不同模块转变成不同的服务!而且服务可以使用不同的技术加以实现!

两者特点如下

SOA实现 微服务实现
企业级,自顶向下实现 团队级,自低向上开展实施
服务由多个子系统组成 一个系统被拆分为多个服务,粒度细
企业服务总线,集中式的服务架构 无集中式架构,松散的服务架构
集成方式复杂(ESB/WS/SOAP) 集成方式简单(HTTP/REST/JSON)
单块架构系统,相互依赖,部署复杂 服务都能独立部署

两者特点可以看出,对于应用本身暴露出来的服务,是和应用一起部署的,即服务本身并不单独部署,服务本身就是业务组件已有的接口能力发布和暴露出来的
其次,微服务架构本身来源于互联网的思路,因此组件对外发布的服务强调了采用HTTP Rest API的方式来进行
微服务的基本思想在于考虑围绕着业务领域组件来创建应用,这些就应用可独立地进行开发、管理和加速。在分散的组件中使用微服务云架构和平台使部署、管理和服务功能交付变得更加简单。

若如果一句话来谈SOA和微服务的区别,即微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业务系统内部实现真正的组件化

最近公司新项目也要使用git进行版本管理,由于之前已经有github在电脑上生存了一个公钥,但是用户名和邮箱都是自己的额,现在需要用公司邮箱重新生成公钥,发现在同一个电脑上使用2个git账号还真折腾了半天功夫

首先需要生成SSH key,由于之前已经存在一个github的公钥,所以还要再生成一个工作的key

1
ssh-keygen -t rsa -C "your-email-address"

注意不要覆盖之前的id_rsa,使用一个新的名字,比如id_rsa_work

把id_rsa_work.pub加到你的work账号上,即把该key加到ssh agent上。由于不是使用默认的.ssh/id_rsa,所以你需要显示告诉ssh agent你的新key的位置

1
ssh-add ~/.ssh/id_rsa_work

但是这个步骤有个问题,总是执行不成功,提示错误如下

1
Could not open a connection to your authentication agent

根据网上提示,出现这样的错误,说明ssh-agent没有启动起来,需要手动启动ssh-agent

1
eval $(ssh-agent)

这个方法确实可以解决问题,但我发现其实只是启动了一个临时的线程服务,电脑重启后就失效了
所以需要将这个服务一直保持启动状态,需要在git启动的配置文件里面加入启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SSH_ENV="$HOME/.ssh/environment"

function start_agent {
echo "Initialising new SSH agent..."
/usr/bin/ssh-agent | sed 's/^echo/#echo/' > "${SSH_ENV}"
echo succeeded
chmod 600 "${SSH_ENV}"
. "${SSH_ENV}" > /dev/null
/usr/bin/ssh-add;
}

# Source SSH settings, if applicable

if [ -f "${SSH_ENV}" ]; then
. "${SSH_ENV}" > /dev/null
#ps ${SSH_AGENT_PID} doesn't work under cywgin
ps -ef | grep ${SSH_AGENT_PID} | grep ssh-agent$ > /dev/null || {
start_agent;
}
else
start_agent;
fi

在这里为止,问题终于解决,一个电脑上使用两个git账号

2015年,参与了几个项目,分布式任务调度管理平台、浙江移动流量项目、浙江移动crm服务化项目,每一个周期都不算长。

分布式任务调度管理:
1、结合quartz实现分布式调度任务管理
2、ZooKeeper作为配置管理中心的开发与应用
3、心跳、监控等系统高可用的实现

浙江流量项目:
1、流量模型的建立与开发
2、抢红包的实现(仍在学习中)

浙江移动crm服务化:
1、账单发票模型的建立与业务流程
2、大账户优化的处理

Quartz是经典的Java版开源定时调度器,项目中作为作业调度管理进行使用。由于之前项目出过一次异常,调试跟进了Quartz中,研究了一下其源码和原理
几个关键概念:
1、Job
表示一个工作,要执行的具体内容。此接口中只有一个方法
void execute(JobExecutionContext context)

2、JobDetail
JobDetail表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。

3、Trigger代表一个调度参数的配置,什么时候去调。

4、Scheduler代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。

最简单的HelloWorld示例

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  HelloWorldMain {
Log log = LogFactory.getLog(HelloWorldMain. class );

public void run() {
try {
//取得Schedule对象
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sch = sf.getScheduler();

JobDetail jd = new JobDetail( "HelloWorldJobDetail" ,Scheduler.DEFAULT_GROUP,HelloWorldJob. class );
Trigger tg = TriggerUtils.makeMinutelyTrigger( 1 );
tg.setName( "HelloWorldTrigger" );

sch.scheduleJob(jd, tg);
sch.start();
} catch ( Exception e ) {
e.printStackTrace();

}
}
public static void main(String[] args) {
HelloWorldMain hw = new HelloWorldMain();
hw.run();
}
}

最近读了两本书:《淘宝技术这十年》和《大型网站技术架构》,都是阿里系出品,前者是叙事+发展,以描述阿里的框架发展为背景,后者主要讲架构的组织和结构,遇到瓶颈和问题的处理和解决方案,在此记录看两本书的读书笔记

架构及发展

大型网站并不是一次就发展起来的,而是一步步演化而来,”不要试图直接建设搭建一个大型网站架构”

应用和数据分离

应用服务器和数据服务器对硬件需求不同,使不同特性服务器承担不同角色

使用缓存

根据80%的数据访问在20%的数据上,将这小部分数据缓存在内存中,减少数据库的压力;例本地缓存分类:有页面缓存、前端页面静态化、前端页面的片段化缓存、以及数据缓存,

服务器集群

使用集群是解决高并发、处理海量数据的常用手段。当一台的服务器处理能力存储空间不足时,不要企图换更大的服务器,更恰当的做法是增加服务器分担原有服务器的压力

数据库读写分离

根据数据库提供的主从热备功能,写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新到从数据库,这样从数据库读数据库的时候,可以通过从数据库读取数据

反向代理和CDN

CDN部署在网络提供商的机房,当用户访问网站服务时,可以从最近的网络提供商机房获取数据;反向代理部署在网站的中心机房,首先访问反向代理服务器缓存的资源

分布式(文件系统和数据库)

分布式数据库是网站数据库拆分的最后手段,在单表数据规模非常庞大时使用

使用NOSQL

非关系型数据库对于分布式需求和海量数据有着先天的处理优势,同时可以减少应用访问数据的麻烦;当前公司业务就使用了redis这种非关系型数据库作为流量经营中政企的数据访问

业务拆分

大型网站会将首页、商铺、订单、买家、卖家分成不同的产品线;包括我当前公司中准备启动的中心模块化都是在向这方向发展

两本书的思维导图

来自网络:大CC

豆瓣上有大牛说其中有些内容是入门级,评分都是7分多,不过对我这种网站架构经验不足的人来说,确实已经达到了拓展视野的目的