Just Do Java

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

麻烦你说下输入百度网址过后整个过程是什么样子的?

发表于 2021-07-27 | 分类于 Java

Hello 大家好,我是阿粉。虽然说金三银四已经过去,但是金九银十快要来了,作为一个居安思危的阿粉,无时无刻不在准备着学习和面试,今天这个题目相信很多面试过的小伙伴都被问到过,问题其实不难,只是如果有的小伙伴没有遇到过,可能不会思考到具体的细节,或者说很多东西可能都知道但是并不知道面试官问这个问题的点在哪,所以会有所欠考虑抓不到重点。下面我们就依次来看一下整个过程到底发生了什么。

阅读全文 »

一个非常好用的Jar,可以帮你省去整合Utils的时间

发表于 2021-07-27 | 分类于 Java

前段时间,阿粉在公司做功能的时候,出现了一个比较别致的需求,需要在页面上显示辛丑年x月x日,于是阿粉就在想,这个天干地支这玩意是不是有人做过封装,能不能整合到Utils里面,以后都一起使用吧,于是就在 Google 上开始搜索有关的内容,结果不搜不知道,一搜吓一跳,直接出现了一个完整的封装好的 Jar 可以直接使用,于是阿粉就把这个 Jar 包的依赖加入到了自己的项目中,而且去源码中扒拉了一下,感觉里面东西真心不错,于是决定分享给大家一下。

Hutool

一个封装了很多功能的 Jar ,阿粉要不是因为需求,估计可能很久不都不一定知道他,我们先来看看阿粉的需求是什么样子的。

阿粉之前的需求是让在后端返回一个所谓农历的干支纪年,而这个东西就不是那么容易了呀,要自己去找一些Utils的话,大家可以看一下。

需要我们去专门弄这个么Utils,然后去计算干支纪年,这是不是有点麻烦了,而我们的 Hutool 这个 Jar 就非常的简单了,

1
2
ChineseDate chineseDate = new ChineseDate(date);
String cyclical = chineseDate.getCyclical();

上面的代码是获取干支纪年,比如他还有获取节日和生效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
	 * 获得节日
	 *
	 * @return 获得农历节日
	 */
	public String getFestivals() {
		return StrUtil.join(",", LunarFestival.getFestivals(this.year, this.month, day));
	}

	/**
	 * 获得年份生肖
	 *
	 * @return 获得年份生肖
	 */
	public String getChineseZodiac() {
		return Zodiac.getChineseZodiac(this.year);
	}

如果你只是认为这个 Jar 只能给你提供这一种帮助那就大错特错了,他内部可是封装了很多很多的方法的。

而且是可以选择引入包,如果你想用 hutool 的所有的 Jar 就可以引入所有的依赖:

1
2
3
4
5
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.2</version>
</dependency>

如果说你只想用其中的某一项功能的时候,那么你就可以挑选其他的 Jar 依赖引入,比如:

1
2
3
4
5
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.6.5</version>
</dependency>

我们就看看 hutool 都有哪些比较牛掰的 Jar 。

  • hutool-aop JDK动态代理封装,提供非IOC下的切面支持

  • hutool-bloomFilter 布隆过滤,提供一些Hash算法的布隆过滤

  • hutool-cache 简单缓存实现

  • hutool-core 核心,包括Bean操作、日期、各种Util等

  • hutool-cron 定时任务模块,提供类Crontab表达式的定时任务

  • hutool-crypto 加密解密模块,提供对称、非对称和摘要算法封装

  • hutool-db JDBC封装后的数据操作,基于ActiveRecord思想

  • hutool-extra 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等)

  • hutool-http 基于HttpUrlConnection的Http客户端封装

  • hutool-log 自动识别日志实现的日志门面

  • hutool-setting 功能更强大的Setting配置文件和Properties封装

  • hutool-script 脚本执行封装,例如Javascript

  • hutool-system 系统参数调用封装(JVM信息等)

  • hutool-json JSON实现

  • hutool-captcha 图片验证码实现

  • hutool-poi 针对POI中Excel和Word的封装

  • hutool-socket 基于Java的NIO和AIO的Socket封装

我们挑几个可以比较常用,而且比较放心的,相对于公司的系统来说,肯定很多人会说,这种 Jar 会不会不安全呢?毕竟互联网时代,安全性要求是非常重要的,既然我们不用那些涉及到一些安全的用例的话,基础的我们还是可以拿过来使用的,不是么?

比如说我们最经典的转型,相信大家肯定使用过 HttpServletRequest ,也就是 HttpServletRequest 获取的Parameter ,然后再转型成我们所需要的类型就像下面:

1
HttpServletRequest.getParameter("")

然后我们就得先转换成 String ,然后再去转换成我们想要的类型。

而在 Hutool 里面,就有一个类提供给我们使用,Convert 类。

1
2
3
4
 转换为字符串:
 long[] b = {1,2,3,4,5};
 String bStr = Convert.toStr(b);
 System.out.println(bStr);

Convert.convert(Class, Object)方法可以将任意类型转换为指定类型,Hutool中预定义了许多类型转换,例如转换为URI、URL、Calendar等等,这些类型的转换都依托于ConverterRegistry类。

在源码里面也是有注释的,我们肯定去扒拉一下看看源码不是么?

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
/**
	 * 转换值为指定类型
	 * 
	 * @param <T> 目标类型
	 * @param type 类型
	 * @param value 值
	 * @return 转换后的值
	 * @since 4.0.0
	 * @throws ConvertException 转换器不存在
	 */
	public static <T> T convert(Class<T> type, Object value) throws ConvertException{
		return convert((Type)type, value);
	}
	
	/**
    	 * 转换值为指定类型
    	 * 
    	 * @param <T> 目标类型
    	 * @param reference 类型参考,用于持有转换后的泛型类型
    	 * @param value 值
    	 * @return 转换后的值
    	 * @throws ConvertException 转换器不存在
    	 */
    	public static <T> T convert(TypeReference<T> reference, Object value) throws ConvertException{
    		return convert(reference.getType(), value, null);
    	}

不得不说,这玩意还真好用,而且你如果在百度上去搜索关于这方面的东西,有些时候还会发现,有人会把里面的源码给拆出来,自己封装成 Utils ,这种可能是觉得引入外面的 Jar 不是很靠谱,于是就把 Jar 里面的方法什么的都给重新封装了一下,作为自己用。

上面这部分阿粉接了个图,发现公司的某个项目中就是直接把 Conver 这个类直接拷贝了一份,然后当做了自己的类进行调用,不得不说,佩服!

提供的算法

Hutool 不光提供了一些基础工具类的使用,同时还提供了一些算法,比如说:HashUtil

  • additiveHash 加法hash
1
2
3
4
5
6
7
public static int additiveHash(String key, int prime) {
		int hash, i;
		for (hash = key.length(), i = 0; i < key.length(); i++) {
			hash += key.charAt(i);
		}
		return hash % prime;
	}
  • 旋转hash
1
2
3
4
5
6
7
8
9
10
11
public static int rotatingHash(String key, int prime) {
    int hash, i;
    for (hash = key.length(), i = 0; i < key.length(); ++i) {
        hash = (hash << 4) ^ (hash >> 28) ^ key.charAt(i);
    }

    // 使用:hash = (hash ^ (hash>>10) ^ (hash>>20)) & mask;
    // 替代:hash %= prime;
    // return (hash ^ (hash>>10) ^ (hash>>20));
    return hash % prime;
}
  • 一次一个hash
1
2
3
4
5
6
7
8
9
10
11
12
13
public static int oneByOneHash(String key) {
    int hash, i;
    for (hash = 0, i = 0; i < key.length(); ++i) {
        hash += key.charAt(i);
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    // return (hash & M_MASK);
    return hash;
}

工具类中融合了各种hash算法,不单单是这么几种,还有好多,像:RS算法hash、JS算法、DEK算法、DJB算法。。。

总之,都是比较不错的呀,至少比我们去百度上面搜索的这种靠谱呀。

而且还包括了加密解密,SecureUtil 其中有对称加密-SymmetricCrypto,也有非对称加密-AsymmetricCrypto。

非常多的内容值得大家去挖掘,如果想看官方文档,阿粉也放给大家,大家可以去看看!

Hutool官方文档

总结

作为一名开发人员,保证开发效率才是第一位的,善于使用工具,对你的开发进度是非常有帮助的,你学会了么?

阅读全文 »

监听 MySQL binlog 实现数据变化后的实时通知

发表于 2021-07-20 | 分类于 Java

Hello 大家好,我是阿粉。不知道大家在日常的工作中有没有遇到这样的场景,很多时候业务数据有变更需要及时加载到缓存、ES 或者发送到消息队列中通知下游服务。

一般遇到这种情况下,在实时性要求不高的场景我们有两种处理模式,一种是写任务定时推送数据同步到缓存中,另一个是下游服务定时自动拉取。这两种模式都依赖服务自己的定时周期时间,很多时候不好设定具体要多久执行一次,定时时间太短在数据没有变化的时候会有很多无效的操作,如果定时时间太长可能很多时候数据的延迟会比较大,某些时候影响也不好。

那有没有一种比较好的方式可以解决这个问题呢?答案当然是肯定的。今天就给大家介绍一下 Canal,基于 MySQL 的 bin log 日志来实时监听数据变化。

阅读全文 »

公司降薪逼迫员工降薪,看程序员如何怒怼公司

发表于 2021-07-20 | 分类于 Java

事情起末

阿粉从来没有想象过这种事情会发生在阿粉的周边,因为阿粉从来没有见过公司会找人谈话,去要求公司的员工去降低薪资,事情发生在阿粉的一个读者的身上,而他也把事情原原本本的给阿粉描述了,阿粉在这里也做一下自己的评价。

事情是这个样子的,阿粉先暂时的把这个粉丝给说成员工君,员工君的公司是一个小型的互联网公司,而且位置是处在二线城市,大家也可能都知道,一些小型的互联网公司如果是做自己的产品的话,肯定需要拉投资,或者说背后有非常有钱的金主可以支持着做某项产品,而在过程中就会出现各种各样的不确定性,比如说:

  • 金主离开(公司倒闭)

  • 产品不合格(公司倒闭)

  • 拉不到投资(公司倒闭)

  • 产品做的好,有投资(公司能继续变的更好)

实际上在这种小型的互联网公司做开发,总是会承担着这样的风险,因为你不知道什么时候就要准备开始新的面试。

而员工君所在的公司是这样的,公司每个月的开工资的时间是下个月的25号,在阿粉的认知中:

  • 第一阶段/ 1—10号:主要是高科技企业、上市企业,外企和国有企业

  • 第二阶段 / 10—15号:一些A股上市企业以及收益相对较好的企业,完整的管理制度以及比较看重人事管理的公司。

  • 第三阶段/15号以后:大部分是小型单位,特别是社会服务行业和劳动密集类型的企业。

25号开工资是什么概念,也就是说,你三月份入职,然后你在4.25的时候才能收到你三月份的工资,也就是说,整整25天,是在你下个月的时候拿不到工资的,不过这也是合法的,为什么说合法,因为劳动法规定的是,三十天以内发放工资。

也就是说,你三月份的工资,最迟的发放时间是4月30日,如果超过这个期限,也就是我们俗称的“拖欠工资”,如果说因为公司经营困难,劳动法规定也是可以适当拖欠一段时间,但是需要公司备案,而很多公司也不进行备案,直接就明着拖欠工资。

而员工君就遇到了这样的事情,公司3月份的工资,一直拖到了5月20日才进行发放,也就是整整拖欠了接近一个月,而这也是导致接下来事情发生的起因。

狼性文化靠什么来支持

据员工君说,小小的公司竟然还推崇华为的“狼性文化”,不得不说,现在的90后和之前的80后不一样了,“狼性文化”的华为,人家加班,给的是钱,你一个小小的公司,能给什么?梦想?没有钱,拿什么谈梦想?

阿粉之前也听在华为的一个大佬说过一句大实话:在华为期间,平时就很少听人谈及情怀,而对于基层的员工来说,谈的只是:经济,有能力多给员工发钱,给出超过行业平均的薪资,实际上是一家公司最能体现对员工关怀的地方。

而如果一个小公司想要推崇华为的“狼性文化”,那么势必你要把 金钱 放在第一位,如果你觉得能力不行,可以辞退,可以招聘你觉得能力足够强的人来进行,但是面试的时候既然谈好了指定的薪资,就认为你这个人和你要的薪资成正比,这还只是新员工,如果试用期觉得不合适,可以进行辞退,但是对老员工来说,可能就没有那么容易了,于是小公司开始想“损招”来逼迫老员工辞职了。

降薪,逼迫员工自己走

上面是一小部分的截图,涉及隐私,阿粉不把所有的聊天记录放上了,事情就是,因为公司感觉目前开发部门的所有人的工资都偏高,所以想要给部门内的所有人进行薪资的调整,于是开完早会的时候,员工君就被公司的小领导给叫到了会议室,在员工君之前,实际上已经有几个员工被约谈后,不愿意降低薪资的,就准备自己走了。结果出现了员工君这样的狠人。

毕竟谁也不是刚出入社会的 “年轻人”了,面对这种情况,我们需要做好完全的准备,员工君比阿粉想的都细致,因为听到了风声,于是在约谈之前,主动拿出了手机,进行了现场的录音,于是在谈论降低薪资的时候,所有的内容全部都在录音文件中,而这块内容,阿粉不得不称赞员工君的机智,毕竟凡事都得讲证据,他给你降薪,万一他说你同意了怎么办?

最终员工君被辞退了,但是员工君就走上了打官司的道路,在历时一个月的官司中,最终判定,降低薪资不合法,并且付N+1的辞退工资,并且赔偿金赔付了2个月的工资。

那么这个实践给了我们什么样子的启示呢?

知道使用劳动法保护自己的合法权益

阿粉为什么这么说呢?因为现在很多年轻朋友,尤其是初入职场的,很多都会被一些公司欺骗,咱们先把好公司防在一边,只谈那些比较操蛋的公司。

  • 基本底薪3000 剩下的都是绩效,奖金等

阿粉不知道大家听到这个怎么想,毕竟程序员和其他的职业不太一样,不像销售那样需要售卖出一些东西才能获得奖金,绩效提成。如果说基本底薪就3000,阿粉不知道这是属于什么语言的程序员,如果你要是刚毕业参加工作,只是为了学习经验,增长见识,这种情况也有,但是一个工作几年的开发人员,一定不会接受这种,外包除外,因为外包很多都是底薪3000,然后加上项目工资是多少等,一系列的薪资才能到达你面试的时候期望薪资。

  • 我们公司试用期不缴纳五险一金

说实话,这块内容阿粉之前从来没有关注过,直到后来不断的有学弟学妹们从学校走出来之后,询问阿粉,为什么公司说是试用期三个月,试用期内不缴纳五险一金呢?

阿粉于是就咨询了一下老同学,发现这样的情况是非常多的,尤其是针对二线城市的小型公司来说,试用期不缴纳五险一金是非常多的,甚至有些小公司,就算转正之后,也就是给你缴纳个五险,也没有一金,这也就是为什么在劳动仲裁的时候,百分之99 都是劳动者状告公司,而且还都能赢,因为资本有时候真的是对打工者无情的打压。

根据《劳动合同法》第十七条、第十九条规定,用人单位和劳动者可以在劳动合同中约定试用期,试用期包含在劳动合同期限内。同时,根据《劳动法》第七十二条和《社会保险法》第四条的规定,用人单位和劳动者必须依法参加社会保险,缴纳社会保险费。

阿粉也不是那种熟记劳动法的人,只是有时候必须要学会用法律来维护自己的合法权益,不是么?

  • 公司现在正处在困难时期,先给大家降低一下薪资

首先这块内容,你先要表示出明确的单方面的降薪你是不同意的,单方降低工资实际上是单方变更劳动合同的行为,是一种严重违法、违约的行为。

但是如果说公司给你调岗降薪,你没有在一个月之内提出异议,那么就相当于是你同意了公司的调岗降薪了,所以,有问题,一定得处理一下,不要拖,尤其是对自己工作这事。

如果有公司说,我书面发出了通知,并且单独谈话也都告诉你们了,这时候就像阿粉的读者员工君一样,我不同意单方面的降薪,第一,我没迟到,第二,我按时完成了我的工作内容,并且在谈话过程中也有录音,即使两方真的闹得不可开胶的话,对簿公堂,你也有的说。

阿粉之前从事的一个公司,在公司最困难的时候,老板都发话,所有员工的钱不能拖,也不用降,哪怕奖金取消,工资还是照常发放,这才是一个当老板要学会的呀。

所以阿粉就算从之前的公司走了,不管是公司的同事还是领导打电话问一些事情,阿粉同样都比较上心,能解决的,还是帮忙解决,针对这种让员工没有一点感情的公司,说实话,打电话阿粉都会拒接。

如何挑选一个好的公司

  1. 看面试官

首先面试官一定是和你接触时间最长的人,因为他需要面试你,不管你面试的时候到底面试的怎么样,一个好的面试官给人的感觉总是不一样的,大家还记得之前在校招上 B 站的招聘人员的一句话对公司有多么大的影响么?

所以既然能当面试官,那么要么是你将来要去部门的领导,要么也算是技术大佬。面试官如果是那种盛气凌人的那种,居高临下的那种,这样的公司去了也免不了勾心斗角,如果你不是很缺一份工作的话,这样的公司可以再考虑一下。

  1. 看HR

和 HR 的对话,能清晰的反应出这个公司是怎么样子的,因为 HR 很多时候都会问一些比较简单的问题,而且会介绍公司的待遇等情况,还会让你发起一些提问,比如你有什么想问我的,而 HR 一般都会把公司的福利待遇等情况都告知你,比如说,双休,加班补贴,加班是否可以打车等等。

如果你觉得面试还可以,而且有对这个公司有兴趣的话,可以多问一些问题,了解公司总是没坏处的。

  1. 看公司氛围

当你去面试的时候,你就能在其中感觉到一点公司的氛围,公司氛围的好坏,能决定你是不是能在这个公司长久的干下去。

所以阿粉希望读者大大们都能够擦亮自己的双眼,找到一个比较好的工作呢!

阅读全文 »

京东宣布涨薪两个月!别人家的公司你酸了吗?B 站半夜宕机,你慌了吗?

发表于 2021-07-13 | 分类于 Java

Hello,大家好,我是阿粉,最近互联网大厂频繁有动静出来,先是滴滴被下架二十多款 APP,后面有快手,字节宣布取消大小周,再有京东宣布涨薪两个月,这边 7.13 号晚 B站也挂了。

阅读全文 »

对比授权机制,你更想用哪种?

发表于 2021-07-09 | 分类于 Java

授权机制,当我们说到这个问题的时候,大家对它的第一印象是在哪个地方呢?是不是曾经某培训机构教授的 SSO 单点登录的,是的没错,而这种 SSO 的单点登录在当年的培训机构中,使用的就是 Session 共享,也就是用 Redis 做中间模拟 Session ,但是授权机制真的有这么简单么?接下来阿粉就来强势对比一下关于授权机制了。

Cookie-Session 认证授权

Cookie-Session 认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。

但是这时候我们就得考虑一下 Cookie 的存活时间了,当我们关闭浏览器的时候,Cookie就会被删除,就算我们调整了 Cookie 的存活时间,但是他依然有很大的弊端,Cookie 是很容易被拦截到的,阿粉之前就看到过某个知名的 OA 系统,就曾经把用户的ID 放到了 Cookie 中,只不过是把 Cookie 里面的键给设置成了 imageUrl,但是实际上这种,看着有点搞人的意思,图片地址是一堆长的字符串,你在前端拦截到之后, 明眼人一眼就能知道这种肯定不是图片路径,而且当我们使用 Cookie 进行用户识别,用户就会很容易受到跨站请求伪造的攻击,也就是我们经常说的 CSRF .

JWT

既然在这里对比 JWT ,我们就得先知道 JWT 是个什么东西,JSON Web Token (JWT) 实际上用大白话说,它就是一种认证机制,让后台知道请求是来自于受信的客户端。

技术都是随着问题出现的,只要有问题,那么很快就会有解决这个问题的技术出现,同样,JWT 出现的只不过比较早而已,因为现在微服务,分布式横行遍布,不管是大公司,还是小公司,很多都开始做分布式的项目,这做分布式也不仅仅是停留在了只存在大公司了,既然选择使用了分布式,那么各种各样的问题就来了。

  • 跨域身份验证

  • 分布式session共享

  • 分布式站点的单点登录

JWT 是个什么玩意

我们先看一下官方网站给的内容,What is JSON Web Token?

1
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA

JSON Web令牌(JWT)是一种开放标准(RFC7519),它定义了一种紧凑且独立的方式,用于在各方之间安全地作为JSON对象传输信息。由于该信息是数字签名的,因此可以验证和信任此信息。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对对对JWTs进行签名.

阿粉就直接用百度翻译了,结果翻译出来竟然差不多,看来百度翻译有时候也没有那么差劲。

那么什么时候需要去使用 JWT 呢?

在官网中,给出了两种情况下去使用 JWT ,Authorization 和 Information Exchange,一种是授权,授权我们都懂,就是当用户登录后,每个后续请求都将包括JWT,允许用户访问该令牌允许的路由、服务和资源,而资源交换,实际上简单的说,就是在数据传输中用 JWT 令牌在安全地在各方之间传输信息

那么我们既然知道了什么时候来使用 JWT, 我们就来看看 JWT 到底是长成什么样子,

JWT 构成:

  • Header 头部

  • Payload 有效载荷

  • Signature 签名

我们从官网上获取了一段的内容,然后逐个来看,

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

既然它说 JWT 是由三段信息构成的,而这三段信息,是用 .来进行隔开的,也就是上面这长串的字符串,

Header 头部,我们看到图里面也给出了,Header 中存储的实际上就是2部分的内容。typ:类型 alg:加密算法,

然后他是对头部进行的 Base64 加密,我就是我们在官网摘下来的第一段的内容,就出现了加密字符串 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 。

Payload 有效载荷

实际上有效载荷实际上就是存储有效信息的地方,那么他都存储了一些什么内容呢?

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号( jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击)

为什么会有这么多,因为在 JWT 的规范中,他告诉我们的是,建议但不强制使用,也就是说,你可以根据自身的应用去选择使用,比如官网给出的,他就没有写全面,就使用了三个:

1
2
3
4
5
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

而实际上这部分的内容却是比较重要的内容。

signature签名信息

实际上这个就是一个组装起来的,将头部和载荷用’.’号连接,再加上一串密钥,经过头部声明的加密算法加密后得到签名。

这个 Header 和 Payload 都是加密过的,而在这个地方它还进行了 “加盐” 的操作,将这三部分用.连接成一个完整的字符串,构成了最终的jwt

jwt其实并不是什么高深莫测的技术,在很多技术人的眼中,可能觉得他会非常的low ,实际上虽然不高端,但是也没有那么 low,认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式,这就是 JWT 的本质实现方式。

JWT 的有点其实很明显,

  • 通过验证签名的方式可以直接在资源服务器本地完成授权校验

  • 在payload中可以包含用户相关信息,实现了token和用户信息的绑定

使用场景一般是用在一次性的身份验证上,千万不要想着去用 JWT 去代替 Session,虽然 JWT 可以设置失效时间,但是在有效期内,它是无法作废的。

OAuth2认证

OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。

其实这个 OAuth 的核心就是向第三方应用颁发令牌,而在 Oauth2 中定义了四种获得令牌的流程,也就是通俗的四种授权方式,但是我们经常使用的也就是那么一种。

  • 授权码

  • 隐藏式(简化)

  • 密码式

  • 客户端凭证

授权码模式

这是在 Oauth 里面的功能算是最完整的,而且流程最严密的授权模式。

授权码模式的步骤:

  • 1.用户访问客户端,后者将前者导向认证服务器

  • 2.用户选择是否给予客户端授权

  • 3.假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码

  • 4.客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见

  • 5.认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)

其实授权码模式就相当于是第三方的应用去先申请一个授权码,然后再用该授权码获取令牌。

总结下来就是四个步骤 : 1:请求授权码 2:返回授权码 3:请求令牌 4:返回令牌

我们给出一个例子,然后分析一下。

1
2
3
4
5
6
7

https://2.com/oauth/authorize? //授权地址
  response_type=code& //参数1:response_type :这里表示授权的类型,此处的值固定为"code"
  client_id=CLIENT_ID& //参数2:client_id :表示客户端的ID
  redirect_uri=CALLBACK_URL& //参数3:redirect_uri :表示重定向URL
  scope=read //参数4:scope: 表示申请的权限范围
  

上面的地址,就相当于第一步,携带所需要的参数请求 网站2,请求获取授权码。

1
https://1.com/callback?code=AUTHORIZATION_CODE //code 授权码

上面的地址就是第二步了,网站2给网站1返回授权码,

1
2
3
4
5
6
7
8

https://2.com/oauth/token?
 client_id=CLIENT_ID&   客户端ID
 client_secret=CLIENT_SECRET& 客户端密钥
 grant_type=authorization_code& 使用的授权模式 authorization_code :授权码模式
 code=AUTHORIZATION_CODE& 授权码
 redirect_uri=CALLBACK_URL  表示重定向URL

上面的地址就到第三步了,用授权码去索要令牌的请求就发送了。

请求发送完成后,2网站收到请求之后,这时候就向 重定向URL 发送以下的 JSON 数据,

1
2
3
4
5
6
7
8
9
{    
  "access_token":"ACCESS_TOKEN", //访问令牌
  "token_type":"bearer",// 令牌类型
  "expires_in":2592000, // 过期时间
  "refresh_token":"REFRESH_TOKEN", // 更新令牌
  "scope":"read", // 权限范围 只读
  "uid":100101, //
  "info":{...} //
}

这时候 访问令牌 我们就要有了,这完成所有的步骤后,我们就拿到了我们访问的令牌了,也就是我们完成了所需要的授权了。

隐藏式

其实隐藏式就是简化版的授权模式,他省略了获取 授权码 的过程,而是直接请求获取 令牌 的过程。

案例如下:

1
2
3
4
5
6
7

https://2.com/oauth/authorize?
  response_type=token& 授权的类型,此处的值固定为"token"
  client_id=CLIENT_ID&  客户端ID
  redirect_uri=CALLBACK_URL& 表示重定向URL
  scope=read  权限范围 只读

上面授权类型直接就是索要令牌,

第二步也很简单,就是直接给你返回你需要的令牌

1
1
1
https://1.com/callback#token=ACCESS_TOKEN

上面的 Token 就是我们需要的令牌了,

密码式

这种为什么称之为 密码式 ,是因为它在请求的时候,是用密码去换令牌,这就需要一个前提,你对这个网站有高度的信用度,如果你不信用他,他给你账号密码作用都不大,给了你也不会授权给它 Token 不是么。

案例步骤如下:

1.请求令牌

1
2
3
4
5
6
7

https://oauth.2.com/token?
  grant_type=password& 授权方式:指定为密码式
  username=USERNAME&  用户名
  password=PASSWORD&  密码
  client_id=CLIENT_ID  客户端ID
  

2.返回令牌

1
1
1
https://1.com/callback#token=ACCESS_TOKEN

这个感觉和隐藏式差距不大,一个是直接要,一个是拿着参数要。

凭证式

这个凭证式的步骤也是比较少的,实际上阿粉感觉这种方式不知道算不算是授权的方式,因为这种模式是客户端以自己的名义向”授权服务提供者”进行认证,但是既然说是,那就暂且的认定他是,

1:请求令牌

1
2
3
4
https://oauth.2.com/token?
  grant_type=client_credentials&  授权方式:凭证式
  client_id=CLIENT_ID&   客户端ID
  client_secret=CLIENT_SECRET 客户端密钥

2.返回令牌

1
1
1
https://1.com/callback#token=ACCESS_TOKEN

这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,也就是说可能出现多个用户共享同一个令牌。

为什么要比较 JWT 和Oauth2 ,因为很多不明所以的人总是会在挑选技术的时候,会把二者拿出来对比,其实上,他们两个没有可比性,因为 JWT 是用于发布接入令牌,并对发布的签名接入令牌进行验证的方法。

OAuth2是一种授权框架,授权第三方应用访问特定资源。

也就是说:

  • OAuth2用在使用第三方账号登录的情况

  • JWT是用在前后端分离, 需要简单的对后台API进行保护

所以你知道怎么选择了么?

文章参考

《阮一峰的网络日志》 《JWT官方文档》

阅读全文 »

2021 入门级的 Java 程序员学习路线图

发表于 2021-07-07 | 分类于 Java

Hello,大家好,我是阿粉,最近看文章发现一个 Java 学习路线图的资料,觉得很不错就拿过来分享给大家,目前这个版本是 1.0 的,之所以说这个是 1.0 的版本主要是因为还有一些高级内容没有加进去,比如 JVM,容器,消息队列,云原生等都还没有提到,不过对于初学者来说这个路线图已经可以上手完成工作了。

阅读全文 »

滴滴出行因违规收集用户个人信息被网信办通知下架!!!

发表于 2021-07-04 | 分类于 Java

Hello 大家好,我是阿粉,最近刚刚过完我党的百年生辰,本是举国同庆的时候,但是往往就有很多不法分子违法违规,这不滴滴就被网信办宣布在所有应用市场下架了么。如下所示

阅读全文 »

手把手叫你搭建一个自己公司的接口文档项目

发表于 2021-07-02 | 分类于 Java

大家还记得之前阿粉给大家推荐的一个写接口文档的神器么?Run-API,前段时间,因为 Show-Doc 进行网站升级,忽然的一天早上 Run-API 失效了,这下整的阿粉就有崩溃了,啥情况,和阿粉对接的前端也有点懵逼,说接口文档忽然访问不了了,阿粉于是赶紧查看,原来是因为阿粉的接口,是完全的依托于 Show-Doc 的服务器进行了发布,为了防止这种情况的出现,阿粉就开始研究关于 Show-Doc 如何在自己的电脑行搭建一个服务,这样如果 Show-Doc 网站再次升级的时候,也就不会出现这么悲剧的事情了。

关于 Show-Doc

ShowDoc 是一个非常适合 IT 团队的在线文档分享工具,它可以加快团队之间沟通的效率,为什么这么说,因为目前现在很多公司都是建立的前后端分离的项目,很多后端都是专职后台业务逻辑的开发,这时候就会出现,每个人写文档的风格不一致,有些人使用 Excel 有些人使用 Word 表格,有些人使用 Swagger 还有人使用 Show-Doc。

而且尤其是在有新老员工离职交接的时候,交接的接口文档都交接不明白,因为很多都是接口写完了,发给前端,功能上线后,文档没用都删除了,这个时候就得去看代码上面的注释,有注释的还好说,没有注释的,那就让你疯狂的崩溃。不知道代码谁写的,这样岂不是很尴尬,这时候我们就可以在公司内部搭建一个文档类型的服务器,好处有很多呀,比如:

  • 一边调试接口、一边自动生成文档

  • 分配项目成员和团队成员,你可以很方便地进行项目文档的权限管理和团队协作

  • 支持多平台客户端,有win客户端、mac客户端、ios、android等,更方便跨平台使用

据说目前一些知名大厂,比如腾讯、华为、百度、京东、字节跳动 这些公司都在使用,阿粉不知道真的假的,有知道的内部员工可以在后台给大家回复一下确认是否使用,还是 Show-Doc 为了宣传效果做的 “虚假宣传”,话不多说,我们开始搞一下安装部署,然后开始使用吧。

安装Show-Doc

上面这个图是需要你安装好本地服务之后,连接本地服务用的,也就是实际写文档的时候用的,搭建软件在下面呢。

下载网址(Windows下)

Show-Doc 版本其实挺多的,因为阿粉之前的服务器已经到期了,我们就先整个 Windows 的版本来整一下,下载 Windows 版本。

还有一个就是 Linux 下有一键脚本安装,比较方便,

windows下安装推荐使用phpStudy集成环境(如果你不用集成环境,请自身确认开启了”php-pdo-sqlite”和”php-sqlite3”扩展)

下载完成之后就是这个鬼样子,阿粉的版本是8.1.1.3,大家安装完成之后,不用担心影响你本地的所有数据信息,放心就行,没啥影响,不用看着有 Apache 和 Mysql 就担心影响本地,只要端口号不冲突,啥问题没有,在他的四个套件中,前三个套件是有用的,第四个 Nginx 做负载均衡 和第一个 Apache 是有冲突的,阿粉安装完成之后,必须让我关闭一个,另外一个才能启动。

但是不影响,启动了 Apache 的时候 Nginx 就算不管他,都能正常的使用,那阿粉就先留下这个坑,如果有问题,到时候再填上他。

点击左侧菜单“网站”,然后点击右边按钮“管理”-“打开根目录”:

这个时候就有比较坑爹的了,如果你去百度,这时候很多人说让我们去下载源码,然后把文件夹直接拷贝进去,这就会出现问题,访问不到,很难受了,有点坑,我们接下来既然打开了根目录,那么就得往里面放东西,需要我们放的就是 show-doc 的源码。

源码地址如下:

show-doc源码github

如果网络不好的,阿粉也给大家准备了,在后台回复showdoc 阿粉会把下载地址给大家,一个是安装包,一个是源码包,一起发给大家。

下载源码包完成之后,进入该文件夹,全选, 把所有文件复制到刚才打开的网站根目录中(例如根目录是D:\phpstudy_pro\WWW)。需要注意的是,不要单纯把showdoc-master这个文件夹复制过去,要进入showdoc-master把里面的文件都复制出来

拷贝进去之后是上图的样子,然后我们点击修改后就可以看到如下,在 WWW 下就是我们要访问的网址。

当我们把这个部署完成之后,我们就能尝试启动一下看看了,

当我们看到这个页面的时候,就是启动从成功了,我们也部署完成了。

提示php-sqlite没安装的问题,你可以打开“管理”-“php扩展”,确保”php-pdo-sqlite”和”php-sqlite3”开启。如果它没开启,你可以点击它开启。

Show-Doc使用

当我们创建的时候,就是上面这个样子

上面有我们需要的 JSON 转 参数表格,JSON 格式化,还有一些 API 模板 比如:

看到这个,你心动了么?快来安装一波试试吧。

阅读全文 »

一个Redis的雪崩和穿透问题,小学妹画了个图,结果入职了

发表于 2021-06-24 | 分类于 Java

阿粉的一个小学妹最近刚从某个小互联网公司跳槽,然后最近面试的挺多的,一个不善言语的小姑娘,技术还是OK的,本来之前是做UI的,但是时间长了,感觉没太大意思,所以就开始学了后端,然后从原有公司慢慢的转为了后端开发人,也就是我们所说的 “程序猿”,最近面试给阿粉谈了谈她的面试经验。阿粉比较印象深刻的一句话就是,我给你画个图,你看一下,这是对面试官说的,事情是什么样子的呢?

你了解 Redis 穿透和雪崩么?

为什么这么说,因为面试官当你说到 Redis 的时候,面试官问的现在已经不是 “你说一下 Redis 的几种数据结构” ,现在面试问的时候,很多都是对 Redis 的实际使用开始问了,比如说,

  • Redis 都有哪些架构模式? 单机版,主从复制,哨兵机制,集群(proxy 型),集群(直连型)

  • 使用过Redis分布式锁么,它是怎么实现的?

  • 使用过Redis做异步队列么,你是怎么用的?有什么缺点?

  • 什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?

而阿粉的小学妹遇到的就是关于 Redis 的缓存穿透和雪崩问题了。这个问题学妹配合了一波自己的 UI 功底图加上口头的解释,于是成功的拿到了这个 Offer,也可能是因为小学妹比较美丽并且技术还过的去。所以,就准备入职了。

我们来看看小学妹到底画了什么图,让面试官问了一波之后就入职了。

缓存穿透

如图:图是阿粉找小学妹专门画出来的,大家看一下

既然我们看完图了,相信打击也都看到了什么是缓存穿透了,也就是说,在我们的缓存系统中,也就是 Redis 中,我们都是拿着我们的 Key 去 Redis 中去寻找 Value 中,如果说我们在 Redis 中找不到我们的数据之后,我们就会去数据库中去寻找我们的数据,如果只是单一请求的话,也不能算是个太大的问题,只能称之为击穿而已,但是如果说要是请求并发量很大的话,就会对我们的数据库造成很大的压力,这其实就称之为缓存穿透,而穿透出现的严重后果,就会是缓存的雪崩了,我们先说穿透,一会再说雪崩。

那么都会有什么情况会造成缓存被穿透呢?

  • 自身代码问题

  • 一些恶意攻击、爬虫造成大量空的命中。

如果有个黑客对你们公司的项目和数据库比较感兴趣,他就可能会给你整出巨多的一些不存在的ID,然后就疯狂的去调用你们的某项接口,这些本身不存在的 ID 去查询缓存的数据的时候,那就是压根没有的,这时候就会有大量的请求去访问数据库,虽然可能数据能支撑一段时间,但是早晚会让人家给你整的凉了。

那么应该怎么去解决缓存穿透的问题呢?

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。

  • 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

  • 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

布隆过滤器实际上是一种比较推荐的方式。

布隆过滤器的实现原理则是这样的:

当一个变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1。查询某个变量的时候我们只要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了,如果这些点有任何一个 0,则被查询变量一定不在;如果都是 1,则被查询变量很可能在。注意,这里是可能存在,不一定一定存在!这就是布隆过滤器的基本思想。

而当你说出布隆过滤器的时候,可能这才是面试官想要问你的内容,这时候你就得好好的和面试官开始聊聊什么事布隆过滤器了。

我们还是继续用大众都想看到的图解来解释布隆过滤器。

字符串 “Java” 在经过四个映射函数操作后在位图上有四个点被设置成了 1。当我们需要判断 “ziyou” 字符串是否存在的时候只要在一次对字符串进行映射函数的操作,得到四个 1 就说明 “Java” 是可能存在的。

注意语言,是可能存在,而不是一定存在,

那是因为映射函数本身就是散列函数,散列函数是会有碰撞的,意思也就是说会存在一个字符串可能是 “Java1” 经过相同的四个映射函数运算得到的四个点跟 “Java” 可能是一样的,这种情况下我们就说出现了误算。

另外还有可能这四个点位上的 1 是四个不同的变量经过运算后得到的,这也不能证明字符串 “Java” 是一定存在的。

而我们使用布隆过滤器其实就是提供一个能迅速判断请求是否有效的拦截机制,判断出请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

而阿粉的小学妹给面试官解释了一波这操作之后,看样子,面试官对这个“程序猿”开始有点印象了,接下来就顺着问了,那什么事缓存的雪崩呢?

缓存雪崩

这时候也就是说,当我们有多个请求访问缓存的时候,这时候,缓存中的数据是没有的,也就是说缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常

他和穿透实际上相似但是又有所不同,相似的地方是都是搞数据库,不同的是缓存穿透是指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库

而解决缓存雪崩的策略也是比较多的,而且都是比较实用的。比如:

  • 给缓存的失效时间,加上一个随机值,避免集体失效。

  • 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间

双缓存策略比较有意思,当请求来临的时候,我们先从 A 缓存中获取,如果 A 缓存有数据,那么直接给他返回,如果 A 中没有数据,那么就直接从 B 中获取数据,直接返回,与此同时,我们启动一个更新的线程,更新 A 缓存和 B 缓存,这就是双缓存的策略。

上述的处理缓存雪崩的情况实际上都是从代码上来进行实现,而我们换个思路考虑呢,也就是从架构的方向去考虑的话,解决方案就是以下的几种了。

  • 限流

  • 降级

  • 熔断

那么怎么实现限流呢?

说到限流降级了,那就不能单纯的去针对 Redis 出现的问题而进行处理了,而实际上是为了保证用户保护服务的稳定性来进行的。

那么为什么要去限流呢?你要单纯的说是为了保证系统的稳定性,那面试官估计得崩溃,这和没说有啥区别,你得举个简单的例子才能正儿八经的忽悠住面试官,比如:

假设,我们当前的程序能够处理10个请求,结果第二天,忽然有200多请求一起过来,整整翻了20倍,这时候,程序就凉了,但是如果第一天晚上的时候,领导给你说,明天你写的那个程序大约会有200多个请求要处理,你这时候是不是得想办法,比如说,能不能再写出另外的一段程序来进行分担请求,这时候其实就相当于需要我们去限流了。

限流算法之漏桶算法

同样的,我们整个图来理解一下这个算法到底是怎么实现的。

如果一桶有一个细眼,我们往里面装水,可以看到水是一滴一滴匀速的下落的,如果桶满了就拒绝水滴继续滴入,没满的话就继续装水,实际上就是这样的水滴实际上就相当于是请求,如果水桶没满的时候,还能继续处理我们进来的请求,当水桶满了的时候,就拒绝处理,让他溢出。

前提是我们的这个桶是个固定的容器,不能随着水的增多桶会变大,要不然那还用什么限流算法。

简单的漏桶算法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LeakyBucket {
        public long timeStamp = System.currentTimeMillis();  // 当前时间
        public long capacity; // 桶的容量
        public long rate; // 水漏出的速度
        public long water; // 当前水量(当前累积请求数)

        public boolean grant() {
            long now = System.currentTimeMillis();
            // 先执行漏水,计算剩余水量
            water = Math.max(0, water - (now - timeStamp) * rate); 
            
            timeStamp = now;
            if ((water + 1) < capacity) {
                // 尝试加水,并且水还未满
                water += 1;
                return true;
            } else {
                // 水满,拒绝加水
                return false;
        }
    }
}

上面的代码是来自悟空,不得不说,这个简单的例子虽然简单,但是吧这个漏桶算法的简单原理描述的还是差不多的,而在这里最需要注意的,就是桶的容量,还有就是水桶漏洞的出水的速度。

既然我们了解了漏桶算法是如何实现限流的,那么必然也会有他处理不来的情况,因为我们已经定义了水漏出的速度,而这时候如果应对突发的流量忽然涌进来,他处理起来效率就不够高了,因为水桶满了之后,请求都拒绝了,都不处理了。

其实我们所说的漏桶算法还可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。

而我们的漏桶算法主要是能够强行限制数据的传输速率。

那么又有什么算法能够不进行强制限制传输速率,并且实现限流呢?

令牌桶算法

我们感谢百度,我从百度图片中找了个一个比较给力的图来描述令牌桶的算法。

令牌桶算法的基本过程是这个样子的:

  1. 用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中

  2. 假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃

  3. 当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络

  4. 如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外

乍一看,怎么感觉这个令牌桶和漏桶这么像,一个是水滴,一个是令牌,实际上不是。

令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。如果令牌桶中存在令牌,则允许发送流量;而如果令牌桶中不存在令牌,则不允许发送流量。

而且他是能够应对突发限制的,虽然传输的速率受到了限制.所以它适合于具有突发特性的流量的一种算法。

而在 Google 开源工具包中的限流工具类RateLimiter ,这个类就是根据令牌桶算法来完成限流。大家有兴趣的可以去看看呀。

漏桶算法和令牌桶算法的区别

漏桶算法与令牌桶算法实际上看起来有点相似,但是不能混淆哈,这就是阿粉在上面说的:

  • 漏桶算法能够强行限制数据的传输速率。

  • 令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输

关于阿粉今天说的这些你学会了么?

文献参考

《百度百科》

阅读全文 »

分布式环境下如何保证 ID 的唯一性

发表于 2021-06-23 | 分类于 Java

前言

首先说下我们为什么需要分布式 ID,以及分布式 ID 是用来解决什么问题的。当我们的项目还处于单体架构的时候,我们使用数据库的自增 ID 就可以解决很多数据标识问题。但是随着我们的业务发展我们的架构就会逐渐演变成分布式架构,那么这个时候再使用数据的自增 ID 就不行了,因为一个业务的数据可能会放在好几个数据库里面,此时我们就需要一个分布式 ID 用来标识一条数据,因此我们需要一个分布式 ID 的生成服务。那么分布式 ID 的服务有什么要求和挑战呢?

阅读全文 »

作为 Java 开发程序员,你知道什么是 Serveless 架构吗?

发表于 2021-06-20 | 分类于 Java

Hello 大家好,我是阿粉,在了解什么是 Serveless 架构之前,我们先看下传统的项目发布和部署的流程是什么样子的。

阅读全文 »

Redis和Mysql如何保证数据一致?面试可以这样说自己的看法

发表于 2021-06-17 | 分类于 Java

阿粉的小学弟最近开始了面试,毕竟也算是工作过一两年的人,现在面试也都开始造飞机了,小学弟开始在面试官面前疯狂造飞机了,也不知道这个飞机好不好用,而开始造飞机的这块内容,就是关于 Redis 的,而面试官问 Redis 的最多的问题,就是如何保证你的 Redis和 MySQL 数据的一致性?接下来我们分别分几种情况来考虑一下这个问题吧。

Redis 和 MySQL 搭配使用在什么地方?

缓存量大但又不常变化的数据

也就是说,当我们在使用 Redis 和 MySQL 的时候,搭配使用的地方就是,数据量比较大,但是这个数据不会经常的变换的位置,比如说,某些商品信息的评论数据,也就是让 Redis 充当 MySQL 的缓存服务器,而要实现的目标也是比较简单的,当客户要查询数据的时候,先访问我们的 Redis ,当 Redis 里面没有数据的时候,从 MySQL 中读取数据,并且存储到 Redis 中。

这个时候 Redis 和 MySQL 的交互就是两部分:

第一部分:同步MySQL数据到Redis

第二部分:同步Redis到MySql

这两部分的内容,实际上就是在一组业务操作中完成的,商品评论信息写入 Redis,如果没有,从 MySQL 中读取,然后写入 Redis。

而接下来的问题就比较严重了,Redis 和 MySQL 数据库数据如何保持一致性?

Redis 和 MySQL 数据库数据如何保持一致性?

为什么会存在这样的一个问题呢?阿粉用网上拿过来的图给大家分析一波。

首先,当我们请求发送到服务器的时候,这个时候,我们先去缓存里面拿我们需要的数据,如果没有的话,我们就去数据库加载数据,加载完成之后,然后再把数据写入到缓存里面。

接下来问题来了,如果你的读和写存在并发的时候,会出现什么样子的问题呢?这个时候,我们就比较尴尬了,压根就没办法保证读和写的顺序,这时候就出现了 Redis 和 MySQL 数据不一致的问题了。

我们准备多种不同的方案来进行不同的分析。

1.先更新数据库,再更新缓存

为什么不考虑这种使用方案呢?

假设我们现在有两个请求 一个是 A 一个是 B ,假设 A 这时候进行请求,A 先更新数据库,接着 B 请求来了, B 更新数据库,结果 B 请求快,B 直接先更新了缓存,这时候缓存更新的内容为 B 更新的,也就是 b ,然后 A 这时候更新完数据库之后,又要更新缓存,这时候 A 更新了缓存,结果最后,缓存里面保存的数据是 a 。

也就是这样的

A —> 更新数据库 —-> 更新缓存为a 我更新数据库慢,我存了个a

B —> 更新数据库 —-> 更新缓存为b 我更新数据库快,我存了个b

本来应该最后更新完成之后的缓存中应该是 b ,结果最后出现了 a ,如果出现这种问题的时候,领导来看的时候,通常挨整的还是程序员自己呀。

这时候,在缓存里面存在的就应该算是脏数据了,所以,这种方案不推荐。

  1. 先更新缓存,再更新数据库

同样的 A B 两个请求,比如说这时候,A请求要进行一个写的操作,而 B 请求要进行一个读取的操作,这时候,A 肯定要删除缓存,就这这个时候,B 来了,我要读取,结果,缓存里面数据不存在,就直接去读数据库,然后把数据库的内容写入到缓存里面了,而这个时候的数据是 A 还没有进行过修改的数据,也就是一个老数据,等读完了之后,A进行了修改,这时候,你的缓存和你数据库中的数据就会出现不一样的情况了。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题,这种方案,同样不可行。

那么我们应该怎么样去保证 Redis 和 MySQL 数据一致性呢?

如何保证 Redis 和 MySQL 数据一致性。

这时候就会有两个在面试的时候,说分布式很容易给自己挖了个大坑的地方,那就是最终一致性和强一致性,而数据库和缓存双写,就必然会存在不一致的问题。

这个命题就会有很感人的地方,你要做出一个选择,如果你选择强一致性,那就不能放缓存,所以,我们也就是仅仅能够保证最终的一致性,而如果选择强一致性,那算了,你别用缓存了。

而且我们说的这个,只是说降低概率发生,而不能完全的避免,但是我们还是要说。

延时双删策略

在写库前后都进行 Redis 的删除操作,并且第二次删除通过延迟的方式进行

那么应该是什么样子的实现逻辑呢?

  • 第一步:先删除缓存

  • 第二步:再写入数据库

  • 第三步:休眠xxx毫秒(根据具体的业务时间来定)

  • 第四步:再次删除缓存。

中间的休眠时间,根据自己的业务时间来进行定夺,这个双删策略实际上就是为了解决你在读数据的时候,生成的过期的数据被第二次写的操作给删除掉。

总有面试官喜欢问为什么要双删,因为第一次删除的是还没更新前的数据,第二次删除则是因为读取的并发性导致的缓存重新写入数据出现的垃圾数据。

这时候总有杠精面试官会问:如果你们的删缓存失败了,怎么办?那不是还是会出现缓存和数据库不一致的情况么?

比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。

这时候我们就需要一个中间件的无私配合了,那就是使用消息来进行重试机制。

步骤:

  1. 业务代码去更新数据库

  2. 数据库的操作进行记录日志。

  3. 订阅程序提取出所需要的数据以及key

  4. 获得该信息尝试删除缓存,发现删除失败的时候,发送消息到消息队列

  5. 继续重试删除缓存的操作,直到删除缓存成功。

其实这个方法和另外一个地方很像,分布式事务的处理方式,就是保证数据的最终一致性,而在分布式事务中,则称之为这种为最大努力通知。

而为什么说是很像,实际上最大努力通知采用的实际上也是 MQ ,但是采用的是 MQ 的 ack 确认机制来进行完成的。

那么最大努力通知又是什么样的流程呢?

  1. 业务方把通知发送给 MQ

  2. 接收通知方监听 MQ

  3. 接收通知方接收消息,业务处理完成回应ack

  4. 接收通知方若没有回应ack则MQ会重复通知,MQ 按照间隔时间从短到长的方式逐步拉大通知间隔,直到达到通知要求的时间上限,比如24小时之后不再进行通知。

  5. 接收通知方可通过消息校对接口来校对消息的一致性

而为什么叫最大努力通知呢,实际上也很容易理解,他并没有从本质上解决问题,只是把问题数目从100 变成了 10 ,毕竟有些内容第一次没处理,第二次就可能会被处理掉。也就是说降低了这种有问题情况的发生,毕竟保证的都是最终一致性。

你面试的时候知道怎么和面试官 Battle 了么?

阅读全文 »

原来传统BIO的局限性在这里!

发表于 2021-06-16 | 分类于 Java

大家都知道传统的 BIO 网络模型有各种各样的缺点,于是就有了关于 NIO 网络模型的出现,更多的人也都开始喜欢使用 Netty 这种框架来进行开发,而摒弃了传统的 BIO 的模型,今天阿粉就给大家说一下为什么这么多人对 BIO 网络模型这么的头疼。

BIO

BIO 网络模型实际上就是使用传统的 Java IO 编程,相关联的类和接口都在 java.io 下。

BIO 模型到底是个什么玩意?

BIO (blocking I/O) 同步阻塞,我们看这个翻译,blocking I/O,实际上就能看出来,就是阻塞,当我们的客户端发起请求的时候,服务端就会开启一个线程,专门为这个客户端提供对应的读写操作,只要客户端发起了,这个服务端的线程就一直保持存在,就算客户端啥也不干,那也在那里开着,就是玩。

不过说一句,现在用 BIO 的不是很多了,因为强制使用的时代过去了,现在还有用 JDK1.4 以下的项目么?估计应该是没有了,阿粉之前曾经维护过一个使用 JRUN 的项目,使用的JDK的版本,就是非常古老的。

我们来整一个服务端和客户端来看看是什么样子的模型。

Server端:

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
public class TimeServer {
    public static void main(String[] args){
        ServerSocket server=null;
        try {
            server=new ServerSocket(18080);
            System.out.println("服务启动 端口:18080...");
            while (true){
                Socket client = server.accept();
                //每次接收到一个新的客户端连接,启动一个新的线程来处理
                new Thread(new TimeServerHandler(client)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

public class TimeServerHandler implements Runnable{
    private Socket clientProxxy;

    public TimeServerHandler(Socket clientProxxy) {
        this.clientProxxy = clientProxxy;
    }

    @Override
    public void run() {
        BufferedReader reader = null;
        PrintWriter writer = null;
        try {
            reader = new BufferedReader(new InputStreamReader(clientProxxy.getInputStream()));
            writer =new PrintWriter(clientProxxy.getOutputStream()) ;
            while (true) {//因为一个client可以发送多次请求,这里的每一次循环,相当于接收处理一次请求
                String request = reader.readLine();
                if (!"GET CURRENT TIME".equals(request)) {
                    writer.println("BAD_REQUEST");
                } else {
                    writer.println(Calendar.getInstance().getTime().toLocaleString());
                }
                writer.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                writer.close();
                reader.close();
                clientProxxy.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Client端:

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
public class TimeClient {
    public static void main(String[] args)  {
        BufferedReader reader = null;
        PrintWriter writer = null;
        Socket client=null;
        try {
            client=new Socket("127.0.0.1",18080);
            writer = new PrintWriter(client.getOutputStream());
            reader = new BufferedReader(new InputStreamReader(client.getInputStream()));

            while (true){//每隔5秒发送一次请求
                writer.println("GET CURRENT TIME");
                writer.flush();
                String response = reader.readLine();
                System.out.println("Current Time:"+response);
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                writer.close();
                reader.close();
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12

服务端:
服务启动 端口:18080...

客户端:
Current Time:2021-6-16 11:37:34
Current Time:2021-6-16 11:37:39
Current Time:2021-6-16 11:37:44
Current Time:2021-6-16 11:37:49
Current Time:2021-6-16 11:37:54
Current Time:2021-6-16 11:37:59
Current Time:2021-6-16 11:38:04

我们使用的是 Client 发送请求指令”GET CURRENT TIME”给server端,每隔5秒钟发送一次,每次 Server 端都返回当前时间。

这就相当于是多个 Client 同时请求 Server ,每个 Client 创建一个线程来进行处理.

Accpetor thread 只负责与 Client 建立连接,worker thread用于处理每个thread真正要执行的操作。

在到了这里的时候,我们就发现了一些事情,感觉不对了有没有,

BIO 的局限性一

  • 每一个 Client 建立连接后都需要创建独立的线程与 Server 进行数据的读写,业务处理。

这时候就会出现什么样子的问题,我们如果说有上千个客户端的时候,我们服务端就会创建上千个线程,有多少客户端,就创建多少个线程,这对于 Java 来说, 代价实在是太大。

如果说我们线程在到达一定数量的时候,我们在做线程的切换的时候,大家可以想象一下对资源的浪费是什么样子的,一个线程和一百个线程甚至超过一千个线程的时候,在线程进行上下文切换的时候,会出现什么样子的问题。

针对某个线程来说,这种 BIO 的模型,在这个线程读取数据的时候,如果没有数据了,那线程就开始了阻塞,为了能够做到响应数据,这个线程在一直阻塞,这时候有新的请求的时候,线程阻塞,好吧,那我只能等,一直处于一个等待不能执行的状态。

BIO 的局限性二

  • 并发数大的时候,会创建非常多的线程来处理连接,系统资源会出现非常大的开销。

BIO 的局限性三

  • 在线程阻塞的时候,会造成资源的浪费。

实际上说是三个局限性,总得来说,他就是一个局限,浪费资源,开销大,这是非常致命的,一个小小的功能的话,你占用的服务器大量的资源,只是硬件上这块的内容,都得增加多少的成本,现在万恶的资本家们,抱着能省就省的原则,又扯远了,我们还是回归到 BIO 上。

而且这种 BIO 的模型,在本质上说,实际上就相当于是一个 1:1 的关系,而这种 1:1 的关系如果在客户端非常多的时候,创建的线程数所浪费的资源是非常巨大的,所以就出现了另外一种模型,NIO 模型。而 NIO 模型实际上目前使用的那可真的是太普遍了,比如说 Netty ,都是选择使用这种模型,不再继续使用 BIO 的模型了。

而关于 NIO 阿粉已经说了很多次,文章都写了好多,阿粉把文章都给大家放在下面,按照顺序阅读,不然阿粉怕大家会有点混乱

什么是BIO,NIO?他们和多路复用器有啥关系?

Java:前程似锦的 NIO 2.0

用Socket编程?我还是选择了Netty

而据说 JDK1.8 之后的 原生NIO API 中,会有空轮询的bug,会让服务器的 CPU 瞬间飙升到100%,具体真假,阿粉反正是没有碰到过,也没有测试出来,真的假的就留给大家去试试了。

文章参考

《田守枝的Java技术博客》 《Netty权威指南》

阅读全文 »

8 条伊隆·马斯克的特斯拉员工必须严格遵守的职场规则

发表于 2021-06-11 | 分类于 Java

Hello 大家好,我是阿粉,如果对币圈有了解的朋友会知道币圈经常会过山车式的跳水和疯涨,而这种情况发生往往很有可能是伊隆·马斯克发了一条动态导致的。马斯克可以说是很传奇的一个人,这位企业家兼 CEO 正在通过 SpaceX 彻底改变航天行业,在特斯拉改变电动汽车的世界,能以一己之力影响币圈,也可以创办 SpaceX 上太空,还可以创办特斯拉造福人类,不得不说对人类的贡献还是很显著的。

这么传奇的一个老板,是怎么看待公司员工的呢?

阅读全文 »

Activity 工作流中的表,原来表示的是这些

发表于 2021-06-10 | 分类于 Java

前几天,阿粉给大家说了关于 Activiti 的使用,后台就有好友私信阿粉说,这些表都不知道是什么意思,不行呀,看不明白呀,于是阿粉就打算再这次给大家讲一下关于 Activiti 的这些表中的字段都是表示的什么意思。

<–more–>

Activiti 的表

表   说明
act_ge_bytearray   通用数据
act_ge_property   流程引擎数据
act_hi_actinst   历史节点表
act_hi_attachment   历史附件表
act_hi_comment   历史意见表
act_hi_detail   历史详情
act_hi_identitylink   历史流程人员
act_hi_procinst   历史流程实例
act_hi_taskinst   历史任务
act_hi_varinst   历史变量
act_id_group   用户信息组
act_id_info   用户信息详情
act_id_membership   组和对应信息关联表
act_id_user   用户信息表
act_procdef_info   流程定义信息
act_re_deployment   部署信息
act_re_model   流程设计模型
act_re_procdef   流程定义数据
act_ru_event_subscr   信息监听
act_ru_execution   运行时流程执行数据
act_ru_identitylink   运行时节点人员数据信息
act_ru_job   定时任务数据
act_ru_task   运行时任务节点
act_ru_variable   流程变量数据

阿粉把之前的表给大家都拿出来了,然后我们一一来看这些字段都是些什么意思。在说这个Activiti的表结构的意思的时候,我们肯定首先要知道 Activiti 的生命周期,生命周期要经过的步骤如下:

1.流程部署 —> 2.启动流程实例 — > 3.执行流程对象(一个流程实例包含多执行对象) —> 4.完成整个流程

而我们要想了解这个流程,就得先从表结构开始了,我们来看看面所有的表结构吧。

大家如果对表结构没有任何的兴趣,那就往后滑,看后面的关于 Activiti 的流程的内容。

1.act_ge_bytearray 通用数据,二进制数据表

保存流程定义图片和xml、Serializable(序列化)的变量,即保存所有二进制数据,特别注意类路径部署时候,不要把svn等隐藏文件或者其他与流程无关的文件也一起部署到该表中,会造成一些错误(可能导致流程定义无法删除)。

字段说明

  • ID_ : 主键ID,也是主键唯一索引

  • REV_: Version(版本) 乐观锁

  • NAME_: 部署的文件名称,如:mail.bpmn、mail.png 、mail.bpmn20.xml

  • DEPLOYMENT_ID_: 部署表ID

  • BYTES_: 部署文件

  • GENERATED_: 是否是引擎生成,0为用户生成 1为Activity生成

在这里我们就要注意到 REV_ 这个乐观锁了,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。好像又开始偏题了,我们回归正题,继续看表。

2.act_ge_property 流程引擎数据表
  • NAME_: 属性名称,也是主键

  • VALUE_: 资源

  • REV_: 乐观锁

  • next.dbid 当 Activiti 使用DbIdGenerator来生成主键时,用来表示Id块的起始值;Id块就是Activiti产生主键时,Id的取值范围,从next.dbid ~ next.dbid+idBlockSize-1 ,默认idBlockSize = 2500

  • schema.version 表示数据结构版本

  • schema.history 表示数据表结构的更新历史

这里面的数据一般情况下是这几个内容,标识的实际上相当于是 Activiti 的版本的一些相关的信息。

3.act_hi_actinst 历史节点表

这个表实际上就是表示的都是历史活动信息,流程流转过的所有节点的记录都在这个表中,但是他是记录的所有节点信息,而在 taskinst 只记录 usertask 内容

  • ID_: 主键ID

  • PROC_DEF_ID_: 流程定义ID

  • PROC_INST_ID_: 流程实例ID

  • EXECUTION_ID_: 执行实例ID

  • ACT_ID_: 节点定义ID

  • TASK_ID_: 任务实例ID 其他节点类型实例ID在这里为空

  • CALL_PROC_INST_ID_: 调用外部流程的流程实例ID

  • ACT_NAME_: 节点定义名称

  • ACT_TYPE_: 节点类型,如startEvent、userTask

  • ASSIGNEE_: 节点签收人

  • START_TIME_: 开始时间

  • END_TIME_: 结束时间

  • DURATION_: 耗时

4.act_hi_attachment 历史附件表
  • ID_: 主键ID

  • REV_: 乐观锁

  • USER_ID_: 用户ID

  • NAME_: 附件名称

  • DESCRIPTION_: 描述信息

  • TYPE_: 附件类型

  • TASK_ID_: 节点实例ID

  • PROC_INST_ID_: 流程实例ID

  • URL_: 附件地址

  • CONTENT_ID_: ACT_GE_BYTEARRAY的ID 二进制数据表的ID(对应关系)

5.act_hi_comment 历史意见表
  • ID_: 主键ID

  • TYPE_: 类型:event(事件)comment(意见)

  • TIME_: 填写时间

  • USER_ID_: 填写人用户ID

  • TASK_ID_: 节点实例ID

  • PROC_INST_ID_: 流程实例ID

  • ACTION_: 行为类型 AddUserLink、DeleteUserLink、AddGroupLink、DeleteGroupLink、AddComment、AddAttachment、DeleteAttachment.

  • MESSAGE_: 用于存放流程产生的信息,比如审批意见

  • FULL_MSG_: 附件地址

6.act_hi_detail 历史详情表
  • ID_: 主键ID

  • TYPE_: 类型 FormProperty–表单 VariableUpdate–参数

  • PROC_INST_ID_: 流程实例ID

  • EXECUTION_ID_: 执行实例ID

  • TASK_ID_: 任务实例ID

  • ACT_INST_ID_: 节点实例ID,ACT_HI_ACTINST表的ID

  • NAME_: 名称

  • VAR_TYPE_: 参数类型

  • REV_: 乐观锁

  • TIME_: 创建时间

  • BYTEARRAY_ID_: ACT_GE_BYTEARRAY表的ID

  • DOUBLE_: 存储变量类型为Double

  • LONG_: 存储变量类型为long

  • TEXT_: 存储变量值类型为String

  • TEXT2_: 此处存储的是JPA持久化对象时,才会有值。此值为对象ID

业务表单中填写的流程需要用到的变量,以及控制流程流转的变量所有的详细信息都会保存在这个历史详情表中。

7.act_hi_identitylink 历史流程人员

这个表其实就比较好了,因为当我们发起各种各样流程的时候,我们需要的永远都是保证数据从哪里来,已经谁发起的,追根溯源好找人呀。

  • ID_: 主键ID

  • GROUP_ID_: 组ID

  • TYPE_: 类型 assignee、candidate、owner、starter 、participant

  • USER_ID_: 用户ID

  • TASK_ID_: 节点实例ID

  • PROC_INST_ID_: 流程实例ID

主要存储历史节点参与者的信息,就是把发起流程的,还有参与过这个流程的人员信息,全部都加到表中。

8.act_hi_procinst 历史流程实例 (画重点的表!)
  • ID_: 主键ID

  • PROC_INST_ID_: 流程实例ID

  • BUSINESS_KEY_: 业务主键,业务表单的ID

  • PROC_DEF_ID_: 流程定义ID

  • START_TIME_: 开始时间

  • END_TIME_: 结束时间

  • DURATION_: 耗时

  • START_USER_ID_: 起草人的ID

  • START_ACT_ID_: 开始节点ID

  • END_ACT_ID_: 结束节点ID

  • SUPER_PROCESS_INSTANCE_ID_: 父流程实例ID

  • DELETE_REASON_: 删除原因

9.act_hi_taskinst 历史任务信息表 (画重点的表!)
  • ID_: 主键ID

  • PROC_DEF_ID_: 流程定义ID

  • TASK_DEF_KEY_: 节点定义ID

  • PROC_INST_ID_: 流程实例ID

  • EXECUTION_ID_: 执行实例ID

  • NAME_: 名称

  • PARENT_TASK_ID_: 父节点实例ID

  • DESCRIPTION_: 描述

  • OWNER_: 签收人(默认为空,只有在委托时才有值)任务的拥有者

  • ASSIGNEE_: 签收人或被委托

  • START_TIME_: 开始时间

  • CLAIM_TIME_: 提醒时间

  • END_TIME_: 结束时间

  • DURATION_: 耗时

  • DELETE_REASON_: 删除原因(completed,deleted)

  • PRIORITY_: 优先级别

  • DUE_DATE_: 过期时间,表明任务应在多长时间内完成

  • FORM_KEY_: 节点定义的formkey,desinger节点定义的form_key属性

10.act_hi_varinst 历史变量表
  • ID_: 主键ID

  • PROC_INST_ID_: 流程实例ID

  • EXECUTION_ID_: 执行实例ID

  • TASK_ID_: 任务实例ID

  • NAME_: 名称

  • VAR_TYPE_: 参数类型

  • REV_: 乐观锁

  • BYTEARRAY_ID_: ACT_GE_BYTEARRAY表的主键

  • DOUBLE_: 存储DoubleType类型的数据

  • LONG_: 存储LongType类型的数据

  • TEXT_: 存储变量值类型为String,如此处存储持久化对象时,值jpa对象的class

  • TEXT2_: 此处存储的是JPA持久化对象时,才会有值。此值为对象ID

11.act_id_group 用户信息组
  • ID_: 主键ID

  • REV_: 乐观锁

  • NAME_: 用户组名称

  • TYPE_: 用户组类型

12.act_id_info 用户信息详情表
  • ID_: 主键ID

  • REV_: 乐观锁

  • USER_ID_: 用户ID

  • TYPE_: 类型

  • KEY_: formINPut名称

  • VALUE_: 值

  • PASSWORD_: 密码

  • PARENT_ID_: 父节点

用户信息详情表,这个表好像有点鸡肋,目前说是还没有用到,

13.act_id_membership 用户与分组对应信息表

总得来说,这个表是真的简单,因为只是表示用户和组之间的对应关系,和很多硬件方面的内容好像很类似,就几个字段。

  • USER_ID: 用户ID

  • GROUP_ID: 用户组ID

14.act_id_user 用户信息表
  • ID_: 主键ID

  • REV_: 乐观锁

  • FIRST_: 用户姓

  • LAST_: 用户名

  • EMAIL_: 邮箱

  • PWD_: 密码

  • PICTURE_ID_: 头像Id

15.act_procdef_info 流程定义信息表
16.act_re_deployment 部署信息表
  • ID_: 主键ID

  • NAME_: 部署文件名

  • CATEGORY_: 分类类别

  • DEPLOY_TIME_: 部署时间

这个表主要就是在部署流程定义时需要被持久化保存下来的信息。

17.act_re_model 流程设计模型表
  • ID_: 主键ID

  • REV_: 乐观锁

  • NAME_: 名称

  • KEY_:模型的关键字 流程引擎用到。比如:FTOA_SWGL

  • CATEGORY_: 类型,用户自己对流程模型的分类。

  • CREATE_TIME_: 创建时间

  • LAST_UPDATE_TIME_: 最后的修改时间

  • VERSION_: 版本

  • META_INFO_: 以json格式保存流程定义的信息

  • DEPLOYMENT_ID_: 部署ID

  • EDITOR_SOURCE_VALUE_ID_: 编辑源值ID

  • EDITOR_SOURCE_EXTRA_VALUE_ID_: 编辑源额外值ID(外键ACT_GE_BYTEARRAY )

18.act_re_procdef 流程定义数据表

这个表示业务流程定义数据表,对应关系和 act_re_deployment 是多对一的关系。

  • ID_: 主键ID

  • REV_: 乐观锁

  • CATEGORY_: 流程定义的Namespace就是类别,该编号就是流程文件targetNamespace的属性值

  • NAME_: 流程名称,该编号就是流程文件process元素的name属性值

  • KEY_: 流程编号,该编号就是流程文件process元素的id属性值

  • VERSION_: 流程版本号,由程序控制,新增即为1,修改后依次加1来完成的

  • DEPLOYMENT_ID_: 部署表ID

  • RESOURCE_NAME_: 流程bpmn文件名称

  • DGRM_RESOURCE_NAME_: png流程图片名称

  • DESCRIPTION_: 描述信息

  • HAS_START_FORM_KEY_: 是否从key启动,start节点是否存在formKey 0否 1是

  • SUSPENSION_STATE_: 是否挂起,1激活 2挂起

19.act_ru_event_subscr 信息监听表
  • ID_: 主键ID

  • REV_: 乐观锁

  • EVENT_TYPE_: 事件类型

  • EVENT_NAME_: 事件名称

  • EXECUTION_ID_: 执行实例ID

  • PROC_INST_ID_: 流程实例ID

  • ACTIVITY_ID_: 活动实例ID

  • CONFIGURATION_: 配置信息

  • CREATED_: 创建时间

20.act_ru_execution 运行时流程执行数据表

这个表实际上就是很多 OA 中会出现的比如说,待办信息的展示。

  • ID_: 主键ID

  • REV_: 乐观锁

  • PROC_INST_ID_: 流程实例ID

  • BUSINESS_KEY_: 业务主键ID

  • PARENT_ID_: 父节点实例ID

  • PROC_DEF_ID_: 流程定义ID

  • SUPER_EXEC_:

  • ACT_ID_: 节点实例ID即 ACT_HI_ACTINST 中ID

  • IS_ACTIVE_: 激活状态,是否存活

  • IS_CONCURRENT_: 是否为并行(true/false)

  • IS_SCOPE_: 范围定义

  • IS_EVENT_SCOPE_: 事件范围

  • SUSPENSION_STATE_: 挂起状态 1激活 2挂起

  • CACHED_ENT_STATE_: 缓存结束状态

21.act_ru_identitylink 运行时节点人员数据信息表

任务参与者数据表。主要存储当前节点参与者的信息。

  • ID_: 主键ID

  • REV_: 乐观锁

  • GROUP_ID_: 分组ID

  • TYPE_: 用户组类型 主要分为以下几种:assignee、candidate、owner、starter、participant。即:受让人,候选人,所有者、起动器、参与者

  • USER_ID_: 用户ID

  • TASK_ID_: 任务Id

  • PROC_INST_ID_: 流程实例ID

  • PROC_DEF_ID_: 流程定义Id

22.act_ru_job 定时任务数据表
  • ID_: 主键ID

  • REV_: 乐观锁

  • TYPE_: 类型

  • LOCK_EXP_TIME_: 锁定释放时间

  • LOCK_OWNER_: 挂起者

  • EXCLUSIVE_:

  • EXECUTION_ID_: 执行实例ID

  • PROCESS_INSTANCE_ID_: 流程实例ID

  • PROC_DEF_ID_: 流程定义ID

  • RETRIES_:

  • EXCEPTION_STACK_ID_: 异常信息ID

  • EXCEPTION_MSG_: 异常信息

  • DUEDATE_: 到期时间

  • REPEAT_: 重复

  • HANDLER_TYPE_: 处理类型

  • HANDLER_CFG_: 标识

23.act_ru_task 运行时任务节点表
  • ID_: 主键ID

  • REV_: 乐观锁

  • EXECUTION_ID_: 执行实例ID

  • PROC_INST_ID_: 流程实例ID

  • PROC_DEF_ID_: 流程定义ID

  • NAME_: 节点定义名称

  • PARENT_TASK_ID_: 父节点实例ID

  • DESCRIPTION_: 节点定义描述

  • TASK_DEF_KEY_: 任务定义的ID

  • OWNER_: 拥有者(一般情况下为空,只有在委托时才有值) 实际签收人

  • ASSIGNEE_: 签收人或委托人

  • DELEGATION_: 委托类型

  • PRIORITY_: 优先级别,默认为:50

  • CREATE_TIME_: 创建时间

  • DUE_DATE_: 耗时

  • SUSPENSION_STATE_: 是否挂起,1代表激活 2代表挂起

24.act_ru_variable 流程变量数据表
  • ID_: 主键ID

  • REV_: 乐观锁

  • TYPE_: 编码类型

  • NAME_: 变量名称

  • EXECUTION_ID_: 执行实例ID

  • PROC_INST_ID_: 流程实例ID

  • TASK_ID_: 节点实例ID(Local)

  • BYTEARRAY_ID_:ACT_GE_BYTEARRAY的ID

  • DOUBLE_: 存储变量类型为Double

  • LONG_: 存储变量类型为long

  • TEXT_: 存储变量值类型为String 如此处存储持久化对象时,值jpa对象的class

  • TEXT2_: 此处存储的是JPA持久化对象时,才会有值。此值为对象ID

Activiti生命周期

1.流程部署 —> 2.启动流程实例 — > 3.执行流程对象(一个流程实例包含多执行对象) —> 4.完成整个流程

上图是个请假流程图,我们按照这个来整点代码来安排一下这个工作请假审批流。

  1. 画个图,发布流程,进行流程部署
1
2
3
4
5
6
rocessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = processEngine.getRepositoryService();
repositoryService.createDeployment()
  .addClasspathResource("activity.cfg.xml")
  .deploy();
Log.info("Number of process definitions: " + repositoryService.createProcessDefinitionQuery().count());
  1. 启动一个流程实例
1
2
3
4
5
6
7
8
9
    Map<String, Object> variables = new HashMap<String, Object>();
    variables.put("employeeName", "Kermit");
    variables.put("numberOfDays", new Integer(4));
    variables.put("vacationMotivation", "I'm really tired!");
    
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("vacationRequest", variables);
    
    Log.info("Number of process instances: " + runtimeService.createProcessInstanceQuery().count());
  1. 执行流程对象
1
2
3
4
5
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {
  Log.info("Task available: " + task.getName());
}

上面代码中的 tasks 实际上可以说是一个任务列表,展示了所有必须由整个用户处理的任务

  1. 完成任务
1
2
3
4
5
Task task = tasks.get(0);
Map<String, Object> taskVariables = new HashMap<String, Object>();
taskVariables.put("vacationApproved", "false");
taskVariables.put("managerMotivation", "We have a tight deadline!");
taskService.complete(task.getId(), taskVariables);

对Activiti来说,就是需要complete任务,这个时候,实际上一个简单的请假流程就已经完成了,这时候,流程实例就会进入到下一个环节中。

而在这中间,我们可以设置一下把请假流程挂起,挂起的时候,就不能创建新流程了,不然就会出现异常,

1
2
3
4
5
6
repositoryService.suspendProcessDefinitionByKey("vacationRequest");
try {
  runtimeService.startProcessInstanceByKey("vacationRequest");
} catch (ActivitiException e) {
  e.printStackTrace();
}

我们通过 RepositoryService 挂起了流程,这个时候,流程不能继续执行,也不会执行异步操作,当我们需要激活这个流程的时候,我们就需要调用:

runtimeService.activateProcessInstanceXXX 方法来对流程进行激活了。

Activiti 的生命周期,就是这么简单,你学会了么?

文章参考

《Activiti使用手册》 《Spring-Activiti代码》

阅读全文 »

手把手教你搭建一个简单的Activity工作流项目

发表于 2021-06-10 | 分类于 Java

前段时间,公司说要做技术分享,于是没周都会安排同事进行技术方面的分享,虽然有时候大部分的人在玩手机,有些同事也在专心致志的在学习,毕竟程序员永远都是保持在学习写代码的路上,JDK都出到16了,尽管你可能现在还是在使用 JDK8 但是还是要继续学习呀。于是阿粉就准备研究一些公司目前没有用到的关系,就学习了一下 Activity 工作流的相关知识,在这里阿粉也分享给大家。

<–more–>

什么是 Activity 工作流引擎 ?

什么是工作流,比如说,我们在公司请假,可能要走蹭蹭审批的流程,从你自己到 Leader,然后从 Leader 到部门经理,然后部门经理再到人事部门,这一系列的流程实际上就相当于是一个工作流程,而这个就是一个工作流的最容易理解的模型。

这肯定官方解读肯定不是这样,不然也太接地气了点,那么什么是 Activity 工作流呢?

工作流(Workflow),指“业务过程的部分或整体在计算机应用环境下的自动化”。是对工作流程及其各操作步骤之间业务规则的抽象、概括描述。在计算机中,工作流属于计算机支持的协同工作(CSCW)的一部分。后者是普遍地研究一个群体如何在计算机的帮助下实现协同工作的。

工作流主要解决的主要问题是:为了实现某个业务目标,利用计算机在多个参与者之间按某种预定规则自动传递文档、信息或者任务。

其实看到百度百科给我们的提示,我们就知道了,实际上工作流就是为了让多个业务目标之间,按照某种规则传递信息。

上面的图是一个请假的流程图,网上的图,不是手动画的,但是表示的意思还是很清晰的。

接下来我们就整一个关于 Activity 的项目来搞一搞吧。

准备工作

  1. 我们先再 IDEA 中装个插件 actiBPM ,直接装就好。

  1. 从 GitHub 上下载源代码 https://github.com/Activiti/Activiti 或者我们直接从官网上下载,https://www.activiti.org/get-started,下载版本的话,新版本也可以,老版本也凑活,我们下载了个比较古老的版本,5.22.

因为网速原因,数据包阿粉已经给大家准备好了,大家回复 Activity 就可以获取下载连接,

数据库在文件中,大家要注意,文件中的数据库是有对应的,mysql,oracle,这些都是不一样的,别直接打开就导入,结果导入半天,报了一大堆错误,还不知道为啥出错。

使用这几个 SQL 的脚本建立完数据库之后,就是上图的这些了,我们来看看都是有哪些表。

  • ACT_HI_*: 这些表包含历史数据,比如历史流程实例, 变量,任务等等

  • ACT_ID_*: 这些表包含身份信息,比如用户,组等等。

  • ACT_RE_*: 表包含了流程定义和流程静态资源 (图片,规则,等等)

  • ACT_RU_*: 包含流程实例,任务,变量,异步任务等运行中的数据

下面的这些表示通过下载的源码包然后进行导入进来的,我们下面在使用 Activity 的时候,我们会直接使用 Activity 设计好流程图,然后我们直接让它帮我们去生成表。

表   说明
act_ge_bytearray   通用数据
act_ge_property   流程引擎数据
act_hi_actinst   历史节点表
act_hi_attachment   历史附件表
act_hi_comment   历史意见表
act_hi_detail   历史详情
act_hi_identitylink   历史流程人员
act_hi_procinst   历史流程实例
act_hi_taskinst   历史任务
act_hi_varinst   历史变量
act_id_group   用户信息组
act_id_info   用户信息详情
act_id_membership   组和对应信息关联表
act_id_user   用户信息表
act_procdef_info   流程定义信息
act_re_deployment   部署信息
act_re_model   流程设计模型
act_re_procdef   流程定义数据
act_ru_event_subscr   信息监听
act_ru_execution   运行时流程执行数据
act_ru_identitylink   运行时节点人员数据信息
act_ru_job   定时任务数据
act_ru_task   运行时任务节点
act_ru_variable   流程变量数据

我们了解了这些表数据都是干啥用的之后,接下来就直接从使用开始吧,毕竟要先看看这用起来是啥样子,才能知道他到底为什么这么香。

这些表如果你自己不拿出来用的话,使用 IDEA 创建关于 Activity 的项目的时候,会给你自动创建表,就类似 Hibernate 一样。

使用 IDEA 创建 Activity 项目

  1. 使用 IDEA 创建一个项目,然后再 POM 文件中加入依赖
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
  <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <!--- Activiti依赖导入 -->
    <dependency>
        <groupId>org.activiti</groupId>
        <artifactId>activiti-spring</artifactId>
        <version>5.18.0</version>
    </dependency>
    <dependency>
        <groupId>org.activiti</groupId>
        <artifactId>activiti-engine</artifactId>
        <version>5.18.0</version>
        <exclusions>
            <exclusion>
                <artifactId>slf4j-api</artifactId>
                <groupId>org.slf4j</groupId>
            </exclusion>
            <exclusion>
                <artifactId>spring-beans</artifactId>
                <groupId>org.springframework</groupId>
            </exclusion>
            <exclusion>
                <artifactId>jackson-core-asl</artifactId>
                <groupId>org.codehaus.jackson</groupId>
            </exclusion>
            <exclusion>
                <artifactId>commons-lang3</artifactId>
                <groupId>org.apache.commons</groupId>
            </exclusion>
            <exclusion>
                <artifactId>commons-lang3</artifactId>
                <groupId>org.apache.commons</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.35</version>
    </dependency>

工作流就是工作流,那是不是得有流程图,就像某些 OA 系统中,要先进行定义流程图,然后自动给你开始搞事情,我们画个流程图来试试。

  1. 在src/main/resources下面新建一个BPMN文件

流程图建立完成之后,就出现了在 IDEA 中从来没有见过的画面,

大家看上图的右半部分,是不是有很多的类似按钮的标志,我们来解释一下他们都是些什么。

  • StartEvent:启动事件元素,启动事件元素就是启动流程实例的,也就是发起一个流程的,是流程的起点。它可以配置的很简单,也可以很复杂。

  • EndEvent:结束事件元素,Activity工作流始于开始任务,止于结束任务

  • UserTask:用户操作的任务

  • ScriptTask: 脚本任务

  • ServiceTask:服务任务

  • MailTask: 邮件任务

  • ManualTask: 手工任务

  • ReceiveTask: 接收任务

  • BusinessRuleTask:规则任务

  • CallActivityTask:调用其他流程任务

  • SubProcess: 子流程

  • Pool: Pool池

  • Lane: Lane小巷 (注意:Lane小巷是放在Pool池里面的)

  • ParallelGateWay: 并行网关

  • ExclusiveGateWay: 排他网关

  • InclusiveGateWay: 包容网关

  • EventGateWay: 事件网关

  • BoundaryEvent: 边界事件

  • IntermediateCatchingEvent: 中间事件

  • IntermediateThrowingEvent:边界补偿事件

  • Annotation: 注释

我们先画一个简单的流程图,然后生成我们需要的表。

图我们换完了,接下来我们就来整个 Activity 的配置文件,然后使用配置文件去生成表。

activity.cfg.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
        <property name="jdbcDriver" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/managementactivity?useUnicode=true&amp;characterEncoding=utf8"></property>
        <property name="jdbcUsername" value="root"></property>
        <property name="jdbcPassword" value="123456"></property>
        <property name="databaseSchemaUpdate" value="true"></property>
    </bean>
</beans>

接下来我们就是用这个配置,去生成我们的数据表,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.activity.zhiyikeji.management;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.junit.Test;

/**
 * @ClassName LeaveFlow
 * @Author
 * @Date 2021/6/11 14:06
 * @Description LeaveFlow
 */
public class LeaveFlow {
    @Test
    public void creatTable(){
        ProcessEngine processEngine = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activity.cfg.xml").buildProcessEngine();
    }

}

执行的时候,我们看一下控制台打印了什么内容,

然后去看看你的数据库,是不是生成成功了,看一看表的数量,一般是24,

从数据包中取出来的 SQL 脚本是多了一个流程定义信息表,这么看来,这个表对使用 Activity 来说意义不大,没他还是可以运行的。

我们已经创建好表之后,接下来我们就来直接进行部署我们画的流程图,然后看看数据库的表中是一些什么样子的数据。

1
2
3
4
5
6
7
8
9
10
11
    /**
     * 部署请假流程
     */
    @Test
    public void deployLeaveFlow(){
        ProcessEngine processEngine = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activity.cfg.xml").buildProcessEngine();
        RepositoryService repositoryService = processEngine.getRepositoryService();
        DeploymentBuilder builder = repositoryService.createDeployment();
        builder.addClasspathResource("zhiyikeji.BPMN");//bpmn文件的名称
        builder.deploy();
    }

嘿,提示完成了,一次过,没出错,

1
2
14:32:50.747 [main] DEBUG org.activiti.engine.impl.interceptor.LogInterceptor - --- DeployCmd finished --------------------------------------------------------
14:32:50.747 [main] DEBUG org.activiti.engine.impl.interceptor.LogInterceptor - 

当我看到这个的时候,我就放心了,我知道,看来,进去了,没啥问题了,那我们就来试试启动一下这个流程。

1
2
3
4
5
6
7
8
9
    /**
     * 启动请假流程
     */
    @Test
    public void startProcess() {
        ProcessEngine processEngine = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activity.cfg.xml").buildProcessEngine();
        RuntimeService runtimeService = processEngine.getRuntimeService();
        runtimeService.startProcessInstanceByKey("leaveProcess");//流程的名称,也可以使用ByID来启动流程
    }

在我们执行完启动请假流程的时候,在 act_ru_task 运行时任务节点表中,就有了我们的一条任务,这样我们就能看到这个任务是什么了。

是不是这么看有点太基础了,这东西看起来也没有我们想象的这么高大上,那我们就找个开源项目,然后把工作流所有的东西都跑起来,然后再去一个个的分析工作流的内容。

阿粉找到了一个开源的项目,项目还是不错的,尤其是得感谢开源出来的大佬shenzhanwang ,先给大家放上图,大家有兴趣的可以下载。

毕竟开源不易,大家对这个有兴趣的可以下载一下看看,阿粉之后再继续给大家了解一下关于 Activity 的里面的一些画 bpmn 图的那些流程上的所有内容。

回复 Activity 获取 Activity的包和源码地址呦。

阅读全文 »

用责任链模式实现 OA 系统中的流程审批

发表于 2021-06-08 | 分类于 Java

Hello 大家好,我是阿粉,工作中我们经常会遇到很多需要上级或者上级的上级一层层审批的流程,作为程序员如果要让你实现这个流程,你会采用什么方式呢?

好了思考一分钟结束,很显然大家一致的回答就是责任链模式。那么什么是责任链模式呢?如何使用责任链模式去完成这个流程呢?下面我们来看一下。

阅读全文 »

Mockito 一个优秀的 Mock 测试框架

发表于 2021-06-05 | 分类于 Java

Hello 大家好,我是阿粉,日常工作中很多时候我们都需要同事间的相互配合协作完成某些功能,所以我们经常会遇到服务或者应用内不同模块之间要互相依赖的场景。比如下面的场景,serviceA 中的 methodA() 方式依赖 serviceB 中的 methodB() 方法返回操作的结果。那如果我们要对自己的methodA() 方法进行编写单元测试,还需要等其他同事的methodB() 方法开发完成才行。那有没有什么办法我们可以跳过或者说模拟方法 B 的输出呢?这就引出了我们今天的主角 Mockito,一个优秀的 Mock 测试框架。

阅读全文 »

分布式锁原来实现起来这么简单

发表于 2021-06-04 | 分类于 Java

阿粉最近迷上了 Redis,为什么呢?感觉 Redis 确实功能很强大呀,一个基于内存的 Key-Value 存储的数据库,竟然有这么多的功能,而阿粉也要实实在在的把 Redis 来弄一下,毕竟面试的时候,Redis 可以说是一个非常不错的加分项。

<–more–>

分布式锁

为什么需要分布式锁?

目前很多的大型项目全部都是基于分布式的,而分布式场景中的数据一致性问题一直是一个不可忽视的问题,大家知道关于分布式的 CAP 理论么?

CAP 理论就是说任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

而我们的系统最终满足的永远都是最终一致性,而这种最终一致性,有些时候有人会喜欢问关于分布式事务,而有些人则偏重在分布式锁上。

分布式锁的种类

  1. 数据库实现分布式锁

  2. 缓存实现分布式锁

  3. Zookeeper实现分布式锁

但是阿粉选择的就是使用缓存来实现分布式锁,也就是我们在项目中最经常使用的 Redis ,谈到 Redis,那真是可以用在太多地方了,比如说:

  • 会话缓存

  • 消息队列

  • 分布式锁

  • 发布,订阅消息

  • 商品列表,评论列表

我们今天就来实现用 Redis 来实现分布式锁,并且要学会怎么使用。

准备工作

1.准备使用 Jedis 的 jar 包,在项目中导入 jar 包。

1
2
3
4
5
6
<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
  1. 直接来写个工具类吧
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
public class RedisPoolUtil {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    private RedisPoolUtil(){}
    /**
     * 
     * @param jedis 
     * @param lockKey 加锁
     * @param requestId 请求的标志位
     * @param expireTime 超时时间
     * @return
     */
    public static boolean tryGetDistributedLock(Jedis jedis,String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }else{
            try{
                Thread.sleep(10);//休眠100毫秒
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return false;
    }
}

jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 这个加锁的姿势才是我们最需要了解的,不然你用的时候都不知道怎么使用。

key:加锁的键,实际上就是相当于一个唯一的标志位,不同的业务,你可以使用不同的标志位进行加锁。

requestId:这个东西实际上就是用来标识他是哪一个请求进行的加锁,因为在分布式锁中,我们要知道一件事,就是加锁的和解锁的,必须是同一个客户端才可以。

而且还有一种比较经典的就是 B 把 A 的锁给释放了,导致释放混乱,如果你不加相同的请求,A 线程处理业务,执行了加锁,锁的过期时间是5s, B线程尝试获取锁,如果 A 处理业务时间超过5s,这时候 A 就要开始释放锁,而B在这时候没有检测到这个锁,从而进行了加锁,这时候加锁的时候,A还没处理完对应业务,当他处理完了之后,再释放锁的话,要是就是直接把 B 刚加的锁释放了,要么就是压根都没办法释放锁。

SET_IF_NOT_EXIST:看字面意思,如果 key 不存在,我们进行Set操作,如果存在,啥都不干,也就不在进行加锁。

SET_WITH_EXPIRE_TIME:是否过期

expireTime:这是给 key 设置一个过期的时间,万一你这业务一直被锁着了,然后之后的业务想加锁,你直接给一直持有这个这个锁,不进行过期之后的释放,那岂不是要凉了。

上面的方法中 tryGetDistributedLock 这个方法也就是我们通常使用的加锁的方法。

解锁

1
2
3
4
5
6
7
8
9
10
11
 public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if ("OK".equals(result)) {
            return true;
        }
        return false;

    }

大家看到这个 script的时候,会感觉有点奇怪,实际上他就是一个 Lua 的脚本,而 Lua 脚本的意思也比较简单。

  1. 先获取锁对应的value值,检查是否与requestId相等

  2. 如果相等则删除锁(解锁)

  3. 执行eval()方法

其实这时候就有些人说,直接 del 删除不行么?你试试你如果这么写的话,你们的领导会不会把你的腿给你打断。

这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,也就是说,这锁就算不是我加的,我都能开,这怎么能行呢?

在这里给大家放一段使用的代码,比较简单,但是可以直接用到你们的项目当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try{
Boolean result = RedisPoolUtil.tryGetDistributedLock(jedis, "xxxxx", uuid, 5000);

if(result) {
        xxxx代码片段
}else{

}

}catch(){

}finally{
RedisPoolUtil.releaseDistributedLock(jedis,"xxxxx", uuid);
}

分布式锁的要求

  1. 满足互斥性。也就是说不管在什么时候,只有一个客户端能够持有锁,不能是多个客户端。

  2. 不能出现死锁。就是说,如果要实现分布式锁,不能说当一个锁没有释放的时候,其他的客户端不能进行加锁,要保证不影响其他的客户端加锁。

  3. 加锁和解锁必须是同一个客户端

分布式的CAP理论

我们先把这个实现方式实现了,然后我们再来说说大家最不愿意看的理论知识,毕竟这理论知识是你面试的时候经常会被问到的。

分布式CAP理论:

加州大学伯克利分校的 Eric Brewer 教授在 ACM PODC 会议上提出 CAP 猜想。2年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP。之后,CAP 理论正式成为分布式计算领域的公认定理。

也就是说,在二十年前的时候,CAP 理论只是个猜想。结果两年之后被证实了,于是,大家在考虑分布式的时候,就有根据来想了,不再是空想了。

什么是分布式的 CAP 理论 ?

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项

这个和(Atomicity)不太一样,因为之前看有些人说,在 CAP 理论中的 A 和数据库事务中的 A 是一样的,单词都不一样,那能一样么?

Availability :分布式中的 A 表示的是可用性,也就是说服务一直可用,而且是正常响应时间。

而你在搭建分布式系统的时候,要保证每个节点都是稳定的,不然你的可用性就没有得到相对应的保证,也谈不上是什么分布式了。只能称之为一个伪分布式。

Consistency: 一致性

也就是说你的更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,这个如果你在使用 Redis 做数据展示的时候,很多面试官都会问你,那你们是怎么保证数据库和缓存的一致性的呢?

毕竟你只是读取的话,没什么问题,但是设计到更新的时候,不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

所以如果你对这个很感兴趣,可以研究一下,比如说:

  1. 延时双删策略

  2. 懒加载 懒加载可采取双删+TTL失效来实现

  3. 主动加载

如果你能在面试的时候把这些都给面试官说清楚,至少感觉你应该能达到你自己的工资要求。

Partition tolerance:分区容错性

分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

其实在 CAP 理论当中,我们是没有办法同时满足一致性、可用性和分区容错性这三个特性,所以有所取舍就可以了。

关于使用 Redis 分布式锁,大家学会了么?

阅读全文 »
1 … 3 4 5 … 32
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

633 日志
116 分类
24 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
  • 沉默王二
  • 子悠
  • 江南一点雨
  • 炸鸡可乐
  • 郑璐璐
  • 程序通事
  • 懿
© 2019 - 2022 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist