1.内容介绍
1. 为什么需要分布式锁;(掌握)
2. 分布式锁概述;(掌握)
3. 常见分布式锁实现;(掌握)
2. 为什么需要分布式锁
2.1.单体项目同步实现
2.1.1.方案说明
在单进程(启动一个jvm)的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
在java中可以通过synchronize和lock等手段来实现。
2.1.2.项目结构
导入locktest.zip,解压导入
2.2.分布式同步问题及解决方案
2.2.1.问题
很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,就没有那么简单啦。
分布式与单机情况下最大的不同在于其不是多线程而是多进程。
多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。
那么原来的方案就不行了
两把锁。
2.2.2.解决方案
分布式锁作用就是在分布式系统,保证某个方法只能在同一时间某个进程的某个线程执行。
在整个分布式环境下都只有一份。
3. 分布式锁概述
3.1.什么是分布式锁
就是在在分布式环境下,保证某个公共资源只能在同一时间被多进程应用的某个进程的某一个线程访问时使用锁。
3.2.几个使用场景分析-一段代码同一时间只能被同一个线程执行
1)库存超卖 (数据不一致) 现象
2)转账
3)Crm添加客户
Select * from t_customer where phone=1333 null Select * from t_customer where phone=1333 = null
Insert into Insert into
方案1:唯一检验-唯一索引 unique
方案2:分布式锁
方案3:消息队列
还有很多很多
3.3.需要什么样的分布式锁
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器-上的一个线程执行。
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
3.4.常见的分布式锁解决方案
3.4.1.思路
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。
分布式锁还是可以将标记存在内存),只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件,zk等做锁与单机的实现是一样的,只要保证标记能互斥就行。
3.4.2.常用分类
1)基于数据库操作
2)基于redis缓存和超时
3)基于zookeeper 临时顺序节点+watch
从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库
基于数据库基本不用,zk或redis要根据项目情况来决定,如果你项目本来就用到zk,就使用zk,否则redis。
3.5.小结
4. 常见分布式锁实现
4.1.基于数据库
4.1.1.mysql innodb行锁
共享锁(S Lock):允许事务读一行数据,具有锁兼容性质,允许多个事务同时获得该锁。
排它锁(X Lock):允许事务删除或更新一行数据,具有排它性,某个事务要想获得锁,必须要等待其他事务释放该对象的锁。
X锁和其他锁都不兼容,S锁值和S锁兼容,S锁和X锁都是行级别锁,兼容是指对同一条记录(row)锁的兼容性情况。
Mysql innodb锁的默认操作:
1)当我们对某一行数据进行查询是会默认使用S锁加锁,如果硬是要把查询也加X锁使用
select * from table where xx=yy for update
2 当我们对某一行数据进行增删改是会加X锁
4.1.2.悲观锁(排他锁)
直接用:基于 select * from table where xx=yy for update SQL语句来实现,有很多缺陷,一般不推荐使用
问题1:多个地方都是使用for update堆select进行排他处理
问题2:我们锁定行只是对该操作进行互斥,其他的地方还要支持查询才OK。
封装分布式锁:
CREATE TABLE `resource_lock` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的资源名',
`owner` varchar(64) NOT NULL DEFAULT '' COMMENT '锁拥有者',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的资源';
resource_name 锁资源名称必须有唯一索引。
必须添加事务,查询和更新操作保证原子性,在一个事务里完成。
伪代码实现:
@Transaction
public void lock(String name) {
ResourceLock rlock = exeSql("select * from resource_lock where resource_name = name for update");
if (rlock == null) {
exeSql("insert into resource_lock(reosurce_name,owner,count) values (name, 'ip',0)");
}
}
使用 for update 锁定的资源。
如果执行成功,会立即返回,执行插入数据库,后续再执行一些其他业务逻辑,直到事务提交,执行结束;
如果执行失败,就会一直阻塞着。
你也可以在数据库客户端工具上测试出来这个效果,当在一个终端执行了 for update,不提交事务。
在另外的终端上执行相同条件的 for update,会一直卡着,转圈圈…
虽然也能实现分布式锁的效果,但是会存在性能瓶颈。
优点:简单易用,好理解,保障数据强一致性。
缺点一大堆,罗列一下:
1)在 RR 事务级别,select 的 for update 操作是基于间隙锁(gap lock) 实现的,是一种悲观锁的实现方式,所以存在阻塞问题。
2)高并发情况下,大量请求进来,会导致大部分请求进行排队,影响数据库稳定性,也会耗费服务的CPU等资源。
当获得锁的客户端等待时间过长时,会提示:
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并发情况下,也会造成占用过多的应用线程,导致业务无法正常响应。
3)如果优先获得锁的线程因为某些原因,一直没有释放掉锁,可能会导致死锁的发生。
4)锁的长时间不释放,会一直占用数据库连接,可能会将数据库连接池撑爆,影响其他服务。
5)MySql数据库会做查询优化,即便使用了索引,优化时发现全表扫效率更高,则可能会将行锁升级为表锁,此时可能就更悲剧了。
6)不支持可重入特性,并且超时等待时间是全局的,不能随便改动。
4.1.3.乐观锁-不上锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
直接用:表中添加一个时间戳或者版本号的字段来实现,update xx set version = new… where id = y and version = old 当更新不成功,客户端重试,重新读取最新的版本号或时间戳,再次尝试更新,类似 CAS 机制,推荐使用。
封装分布式锁:
CREATE TABLE `resource` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '资源名',
`share` varchar(64) NOT NULL DEFAULT '' COMMENT '状态',
`version` int(4) NOT NULL DEFAULT '' COMMENT '版本号',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='资源';
伪代码实现:
Resrouce resource = exeSql("select * from resource where resource_name = xxx");
boolean succ = exeSql("update resource set version= 'newVersion' ... where resource_name = xxx and version = 'oldVersion'");
if (!succ) {
// 发起重试
}
实际代码中可以写个while循环不断重试,版本号不一致,更新失败,重新获取新的版本号,直到更新成功。
4.1.4.基于数据库索引的唯一性
思路:利用主键或唯一索引唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。
use test;
CREATE TABLE `DistributedLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性
lock(name,desc){
insert into DistributedLock(`name`,`desc`) values (#{name},#{desc});
}
unlock(name){
delete from DistributedLock where name = #{name}
数据库是单点?搞两个数据库,数据之前双向同步,一旦挂掉快速切换到备库上。
没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
非阻塞的?搞一个 while 循环,直到 insert 成功再返回成功。
非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
获取:再次获取锁的同时更新count(+1).
释放:更新count-1,当count==0删除记录。
非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁。
比较好的办法是在程序中生产主键进行防重。
4.2.基于Redis
特点:CAP模型属于AP | 无一致性算法 | 性能好
开发常用,如果你的项目中正好使用了redis,不想引入额外的分布式锁组件,推荐使用。
业界也提供了多个现成好用的框架予以支持分布式锁,比如Redisson、spring-integration-redis、redis自带的setnx命令,推荐直接使用。
另外,可基于redis命令和redis lua支持的原子特性,自行实现分布式锁。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.2.3</version>
</dependency>
RLock rLock = RedissionUtils.getInstance()
.getRLock(redisson, "goods" + goodsId);
try{
rLock.lock();
System.out.println(Thread.currentThread().getName()+" get lock!");
Goods goods = goodsMapper.laodById(goodsId);
System.out.println(goods);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(goods.getCount()+":"+num);
if (goods.getCount()>=num){
goodsMapper.updateNum(params);
System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
}
}catch (Exception e){
e.printStackTrace();
}
finally {
if (rLock != null) {
rLock.unlock();
System.out.println(Thread.currentThread().getName()+" unlock!");
}
4.3.ZooKeeper入门
4.3.1.是什么
ZooKeeper是Apache下的一个Java开源项目(最初由Yahoo开发, 后捐献给了Apache)。
ZooKeeper的原始功能很简单,基纡它的层次型的目录树的数据结构,并通过对树上的节点进行有效管
理,可以设计出各种各样的分布式集群管理功能。此外, ZooKeeper本身 也是分布式的。
4.3.2.Zk数据模型
Zookeeper会维护一个具有层次关系的树状的数据结构,它非常类似于一个标准的文件系统,如下图所
示:
同一个目录下不能有相同名称的目录节点
4.3.3.节点分类
1.持久无序节点(PERSISTENT)
持久节点,创建后一直存在,直到主动删除此节点。
2.持久顺序节点(PERSISTENT_SEQUENTIAL)
持久顺序节点,创建后一直存在,直到主动删除此节点。在ZK中,每个父节点会为它的第一级子节点维护一份时序,记录每个子节点创建的先后顺序。
3.临时无序节点(EPHEMERAL)
临时节点在客户端会话失效后节点自动清除。临时节点下面不能创建子节点。
4.顺序临时节点(EPHEMERAL_SEQUENTIAL)
临时节点在客户端会话失效后节点自动清除。临时节点下面不能创建子节点。父节点getChildren会获得顺序的节点列表
4.3.4.安装
① 官方下载地址:http://mirrors.cnnic.cn/apache/zookeeper/
下载后获得,解压即可安装。
② 安装配置
把conf目录下的zoo_sample.cfg改名成zoo.cfg,这里我是先备份了zoo_sample.cfg再改的名。修改zoo.cfg的值如下:
dataDir=D:/zookeeper-3.4.9/data/data
dataLogDir=D:/zookeeper-3.4.9/data/log
③启动
点击bin目录下的zkServer.cmd 这时候出现下面的提示就说明配置成功了。
④ 图形界面-ZooViewer
https://blog.csdn.net/u010889616/article/details/80792912
4.3.5.代码操作-zkclient
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
public class ZkTest {
@Test //持久化节点
public void test1() throws Exception {
//创建客户端
ZkClient client = new ZkClient("127.0.0.1:2181",5000);
//操作节点
// client.createPersistent("/yhptest");
List<String> children = client.getChildren("/");
for (String child : children) {
System.out.println(child);
}
//关闭
client.close();
}
//临时节点
@Test
public void test2() throws Exception {
//创建客户端
ZkClient client = new ZkClient("127.0.0.1:2181",5000);
//操作节点
client.createEphemeral("/yhptest/dbl");
List<String> children = client.getChildren("/yhptest");
for (String child : children) {
System.out.println(child);
}
//关闭
client.close();
}
@Test
public void test3() throws Exception {
//创建客户端
ZkClient client = new ZkClient("127.0.0.1:2181",5000);
//操作节点
client.createPersistentSequential("/yhptest/test","x1");
client.createPersistentSequential("/yhptest/test","x2");
client.createPersistentSequential("/yhptest/test","x3");
client.createPersistentSequential("/yhptest/test","x4");
List<String> children = client.getChildren("/yhptest");
for (String child : children) {
System.out.println(child);
}
//关闭
client.subscribeChildChanges("/yhptest", new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> list) throws Exception {
System.out.println("jjjjj");
}
});
client.subscribeDataChanges("/yhptest/test0000000002", new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
//数据改变
}
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("数据删除");
}
});
client.close();
}
}
4.4.基于zookeeper
特点:CAP模型属于CP | ZAB一致性算法实现 | 稳定性好
开发常用,如果你的项目中正好使用了zk集群,推荐使用。
业界有Apache Curator框架提供了现成的分布式锁功能,现成的,推荐直接使用。
另外,可基于Zookeeper自身的特性和原生Zookeeper API自行实现分布式锁。
4.4.1.方案1 临时节点+重试
4.4.2.方案2 临时顺序节点+watch(下一个监听上一个)
package cn.itsource.lock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class ZookeeperDistributedLock implements ItsourceLock {
ZkClient client = new ZkClient("127.0.0.1:2181",
5000);
CountDownLatch cdl = new CountDownLatch(1); //不为零阻塞住,不让他往下走
//父节点路径
String parent = "";
//当前节点路径
String currentPath = "";
//1 goods
// lock_goods_id 父节点(持久节点)
// lock_goods_id_001
// lock_goods_id_002
@Override
public void lock(String resourceName) {
parent = "/"+resourceName;
//判断父节点是否存在,如果不存在要创建一个持久节点
if (!client.exists(parent)){
client.createPersistent(parent,"root");
}
//前面的节点都处理完成,自己变成第一个节点才加锁成功。
if (!tryLock(resourceName)){
lock(resourceName);
}
}
@Override
public void unlock(String resourceName) {
//自己操作完毕,删除自己,让下一个节点执行。
System.out.println(currentPath);
System.out.println(System.currentTimeMillis());
System.out.println(client.delete(currentPath));
client.close();
}
@Override
public boolean tryLock(String resourceName) {
//创建子节点-临时顺序节点
if (StringUtils.isEmpty(currentPath)){
currentPath = client
.createEphemeralSequential(parent + "/test", "test"); //test0001
}
//如果是第一个节点,就获取到锁了。
List<String> children = client.getChildren(parent);
System.out.println(currentPath+"jjj");
for (String child : children) {
System.out.println(child);
}
Collections.sort(children);
///goods_1/test0000000003jjj
//test0000000003
if (currentPath.contains(children.get(0))){
return true;
}else{
//如果不是第一个节点,监听前一个节点,要再这儿等待,知道被触发,再次判断是否是第一个节点进行返回就OK
String str = currentPath.substring(
currentPath.lastIndexOf("/")+1);
System.out.println(str);
int preIndex = children.indexOf(str)-1;
String prePath = parent+"/"+children.get(preIndex);
client.subscribeDataChanges(prePath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
//让他-1 变为零
cdl.countDown();
}
});
//一直等待,直到自己变成第一个节点
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
}
}
5.课程总结
5.1.重点
5.2.难点
5.3.如何掌握?
1.多多理解
2.学会看说明手册
5.4.排错技巧(技巧)
1…
2…
6.课后练习
1.总结
2.课堂作业
7.面试题
8.扩展知识或课外阅读推荐(可选)
8.1.扩展知识
8.2.课外阅读
微服务那点儿事情
https://blog.csdn.net/w05980598/article/details/79007194/