锁类型
1.排他锁
简称X锁,又称为独占锁或写锁。一般针对写操作被加上锁的资源,在使用的时候只允许对应的事务使用。不允许别的事务使用,直到对应事务释放这个锁,别的事务才能访问这个资源
zookeeper就是使用这个解决分布式锁
2.共享锁
简称S锁,一般是针对读取操作。加上共享锁之后,数据对所有事物都可见。也就是别的事务或者线程都可以使用这个锁
zookeeper锁执行机制
1.定义锁(创建锁)
java中定义锁有两种方式:
- synchronized
- JDK5的ReentrantLock,重入锁(自己抢到的锁,自己还可以再抢到这个锁,别人抢不到
2.获取锁
ookeeper中创建临时节点。
- 如果是读请求,就创建共享锁。也就是多个请求都可以使用同一个资源
- 如果是写请求,就创建排他锁,这样就可以保证一个数据的修改只会被一个事务在同一时间操作
3. 释放锁
- 获取锁的客户端岱机,会自动断开连接,就会删除这个临时节点,也就释放锁了
- 正常业务逻辑执行完毕,可以手动删除这个临时节点
Redis分布式锁
**分布式锁:**控制分布式系统有序的去对资源进行操作,通过互斥的方式保持一致.
例子:假入共享资源是ATM机,分布式系统就是要取钱的人,分布式锁 就是保证这个ATM机一次只有一个人可以使用,一个人使用的时候就会将这个装ATM机的屋子锁上,第二个人想要进去就需要有这个钥匙来打开这个门。由于没有钥匙,所以就得等到第一个人出来,然后第二个人拿着钥匙进去。以此类推.
解决分布式锁的核心思路
在多台服务器集群上,只能保证一个JVM进行操作
分布式锁解决方案
- 基于数据库
- 基于redis实现分布式锁(setnx方法)。setnx可以存入值,如果存入值成功就返回1。如果存入的key已经存在了,就返回0。所以多个jvm同时set一个键,只要成功就可以返回1.就可以继续执行
- 基于Zookeeper实现,Zookeeper:是一个分布式协调工具。在分布式解决方案中可以实现
消息中间件
,注册中心
,分布式锁
(多个jvm,在zk上创建相同的临时节点,因为临时节点的路劲是保证唯一的,只要谁能创建节点成功,就会获取锁。没有创建成功节点,就会进行等待,当释放锁的时候,采用事件通知jvm重新获取锁的资源)
初步理解redis如何实现分布式锁
多个客户端(JVM),使用setnx命令,在redis上创建相同的key,因为redis的key不允许重复,只要谁创建key成功,就能获得这个锁,没有创建成功,就会进行等待
核心思路:保证只有一个JVM进行操作
重点分析redis实现分布式锁
setnx命令的作用:也可以做写入key操作,可以获取返回结果。如果返回结果时1,就表示key不存在,写入成功。如果返回结果是0,表示key存在,写入失败不覆盖
怎么释放锁?
这也是和zk实现分布式锁最大的区别
在执行完操作的时候,删除key,如果删除失败,就会导致思索,所以还需要给这个key设置有效时间,有效时间就是防止死锁.
如果直接删除key,可能会出现这种情况,a获取的锁被b删除了,所以需要保证是自己创建的和删除自己的.
代码实现
代码实现的重点是:
1.在释放锁的时候,需要判断是否是对应的线程释放自己的锁,也就是不能出现a创建锁,结果被b释放了
1.redis在IDEA中的配置
依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
yml配置文件
server:
port: 8084
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
datasource:
username: root
url: jdbc:mysql://localhost:3306/xxx?characterEncoding=utf8&serverTimezone=UTC
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
# redis.cluster集群的节点
cluster:
nodes: 39.97.252.228:8001,39.97.252.228:8002,39.97.252.228:8003,39.97.252.228:8004,39.97.252.228:8005,39.97.252.228:8006,39.97.252.228:8007,39.97.252.228:8008
jedis:
pool:
# 连接池最大数量
max-active: 10
# 连接池最小空闲连接
min-idle: 1
max-idle: 2
# 连接池最大阻塞时间
max-wait: -1
password: 123456
host: 39.97.252.228
timeout: 1000
commandTimeout: 5000
因为redis集群加密了,所以的有配置类配置一下
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashSet;
import java.util.Set;
@Configuration
@ConditionalOnClass({JedisCluster.class})
public class RedisConfig {
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.commandTimeout}")
private int commandTimeout;
@Bean
public JedisCluster getJedisCluster() {
System.out.println(clusterNodes+"******************");
String[] cNodes = clusterNodes.split(",");
Set<HostAndPort> nodes = new HashSet<>();
//分割出集群节点
for (String node : cNodes) {
String[] hp = node.split(":");
nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
}
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//创建集群对象。没有密码的请使用这一个
// JedisCluster jedisCluster = new JedisCluster(nodes,commandTimeout);
//有密码的请使用这一个。 我这里是redis有密码的所以我使用的这一个
return new JedisCluster(nodes,commandTimeout,commandTimeout,5,password, jedisPoolConfig);
}
}
创建锁与释放锁
package com.soa.lock.coderymy;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisCluster;
import java.util.UUID;
public class LockRedis {
@Autowired
private JedisCluster jedisCluster;
/**
* 基于redis实现的分布式锁的代码思路
* 获得锁
* 释放锁
*/
//定义key的名字
private String redisLockKey = "redis_lock";
/**
* 获得锁
* @param acquireTimeout
* @param timeout
* @return
*/
public String getRedisLock(String lockKey, Long acquireTimeout,Long timeout){
/*
* 1. 建立redis连接好几种都行
* 2. 定义redis的key对应的value的值,可以使用uuid生辰(不唯一)。在释放锁的时候使用
* 3. redis实现分布式锁,定义两个超时的时间问题:
* 1. 获取锁之前:获取锁的超时时间,在尝试获取锁的时候,如果在规定的时候还没有获取到就放弃
* 2. 获取锁成功之后:key对应有有效期
* 4. 使用循环机制,保证重复进行尝试获取锁(乐观锁的原理)
* 5. 使用setnx命令插入对应的redisLockKey,如果返回为1就获取锁成功
* */
String retIdentifierValue = null;
try {
//创建value
String identifierValue = UUID.randomUUID().toString();
//创建key
String lockName = redisLockKey+lockKey;
//创建key_value的存活时间
int expriseLock = (int)(timeout/1000);
//创建获取锁的循环等待时间
Long endTime = System.currentTimeMillis()+acquireTimeout;
//进入循环
while ((System.currentTimeMillis() < endTime)){
//使用setnx判断key是否存在("redis_lock",identifierValue )
if(jedisCluster.setnx(lockName,identifierValue)==1){
jedisCluster.expire (lockName,expriseLock);
retIdentifierValue = identifierValue;
return retIdentifierValue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return retIdentifierValue;
}
/**
* 解锁
* 必须验证身份再解锁
* @param lockKey
* @param identifier
* @return
*/
public boolean unRedisLock(String lockKey,String identifier){
boolean flag = false;
try {
//定义锁的名字
String lockName = "redis_lock"+lockKey;
// 3.如果value与redis中一直直接删除,否则等待超时
if (identifier.equals(jedisCluster.get(lockName))) {
jedisCluster.del(lockName);
System.out.println(identifier + "解锁成功......");
}
}catch (Exception e){
e.printStackTrace();
}
return flag;
}
}
使用
import com.soa.lock.coderymy.LockRedis;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class LockService {
LockRedis lockRedis = new LockRedis();
public void seckill() {
String identifier = lockRedis.getRedisLock("itmayiedu", 5000l, 5000l);
if (StringUtils.isEmpty(identifier)) {
// 获取锁失败
System.out.println(Thread.currentThread().getName() + ",获取锁失败,原因时间超时!!!");
return;
}
System.out.println(Thread.currentThread().getName() + "获取锁成功,锁id identifier:" + identifier + ",执行业务逻辑");
try {
// Thread.sleep(30);
} catch (Exception e) {
}
// 释放锁
boolean releaseLock = lockRedis.unRedisLock("itmayiedu", identifier);
if (releaseLock) {
System.out.println(Thread.currentThread().getName() + "释放锁成功,锁id identifier:" + identifier);
}
}
}
测试
@Test
public void test2(){
LockService lockService = new LockService();
for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lockService.seckill();
}
}).start();
}
}
问题的总结
1.为什么要设置redis的key值的过期时间
为了解决死锁问题,也就是有一种可能就是释放锁失败,这样的话,就会造成接下来的操作都无法获取锁
2.一般这个超时时间可以自己判断进行设置,一般都是要求对应的业务执行完毕的时间紧
zookeeper分布式锁
1.zookeeper中的watch监听器
2.临时顺序节点
3.写操作排他锁,读操作共享锁
分布式锁的使用场景
案例:需要生成订单号。使用UUID、时间错+业务id(唯一,不允许重复)
幂等:不重复,唯一
生成订单号
public class OrderNumGenerate{
private int count;
public String getNumber(){
SimpleDataFoemat simpt=new SimpleDataFormat("yyyy-MM-dd hh-mm-ss");
return simpt.format(new Date())+"-"+ ++count;
}
}
问题
生成订单号,使用多线程的方法会出现线程安全问题。也就是在同一时间生成的时间戳等,可能会导致生成的订单号并不唯一。可能会出现很多的相同的
所以多线程共享同一个全局变量,可能会受到其它线程的干扰,这就是线程安全的问题
zookeeper
什么是Zookeeper
是一个分布式协调工具,可以实现很多功能,比如注册中心,分布式锁,负载均衡命名服务,分布式通知/协调,发布订阅(MQ),集群环境的选举策略(哨兵机制)
zk的存储数据结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x9VKEw3R-1576905823867)(D:\总结\分布式事务和锁\img\zookeeper数据结构2.png)]
Zookeeper实现分布式锁
- 使用zk创建临时节点
- 谁能创建节点成功,谁就可以拿到锁,就能生成订单号。
- 释放锁,临时节点。(和会话连接保持一样的状态,只要会话结束,就会删除这个临时节点)
总结: 在zk上创建临时节点,只要谁能创建节点成功,其它没有创建成功就等待。其它服务器使用事件监听,获取节点通知。只要节点被删除,就应该去获取锁资源
代码实现
1.导入依赖
<dependency>
<groupId><u>com</u>.101tec</groupId>
<artifactId><u>zkclient</u></artifactId>
<version>0.10</version>
</dependency>
2.创建锁的接口
public interface Lock {
//获取到锁的资源
public void getLock();
// 释放锁
public void unLock();
}
3.创建实现类
//将重复代码写入子类中..
public abstract class ZookeeperAbstractLock implements Lock {
// <u>zk</u>连接地址
private static final String CONNECTSTRING = "127.0.0.1:2181";
// 创建<u>zk</u>连接
protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
protected static final String PATH = "/lock";
public void getLock() {
if (tryLock()) {
System.out.println("##获取lock锁的资源####");
} else {
// 等待
waitLock();
// 重新获取锁资源
getLock();
//也就是等待到信号量通知可以获取锁了,然后继续获取锁。这里并没有导致死循环,因为只要获取到锁,就会输出,从而不会继续执行else的代码
}
}
// 是否获取锁,获取锁成功true,失败false
abstract boolean tryLock();
// 等待
abstract void waitLock();
public void unLock() {
if (zkClient != null) {
zkClient.close();
System.out.println("释放锁资源...");
}
}
}
3.创建子类,来实现tryLock()和waitLock()方法
public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
private CountDownLatch countDownLatch = null;
@Override
boolean tryLock() {
try {
//这里进行尝试创建锁,也就是使用同一个path创建锁,成功这里就返回true。继续指向上一个类的方法。否则返回false
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
// e.printStackTrace();
return false;
}
}
@Override
void waitLock() {
//创建一个事件监听的方法
IZkDataListener izkDataListener = new IZkDataListener() {
//当监听的节点被删除的时候走这里
public void handleDataDeleted(String path) throws Exception {
// 监听到节点被删除了,就唤醒被等待的线程
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
//当监听的方法被修改走这里
public void handleDataChange(String path, Object data) throws Exception {
}
};
// 注册事件
zkClient.subscribeDataChanges(PATH, izkDataListener);
if (zkClient.exists(PATH)) {
//也就是已经有人在用这个锁,就创建信号量
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
// 删除监听
zkClient.unsubscribeDataChanges(PATH, izkDataListener);
}
}
4.测试
public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
// 使用lock锁
// private java.util.concurrent.locks.Lock lock = new ReentrantLock();
private Lock lock = new ZookeeperDistrbuteLock();
public void run() {
getNumber();
}
public void getNumber() {
try {
lock.getLock();
String number = orderNumGenerator.getNumber();
System.out.println(Thread.currentThread().getName() + ",生成订单ID:" + number);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unLock();
}
}
public static void main(String[] args) {
System.out.println("####生成唯一订单号###");
// OrderService orderService = new OrderService();
for (int i = 0; i < 100; i++) {
new Thread( new OrderService()).start();
}
}
}
总结
为什么需要使用分布式锁
使用最多的一个例子就是,在并发的情况下,创建业务id。使用时间戳或者怎么样,可能会创建重复的业务id。所以需要使用分布式锁来锁住正在创建的业务id。以防止创建相同的id
什么是Zookeeper
Zookeeper是分布式的一套协调工具,它支持了很多的分布式解决方案。如注册中心,分布式锁,负载均衡命名服务,分布式通知/协调,发布订阅(MQ)等等。都可以得到解决
使用Zookeeper实现分布式锁的原理
创建锁
使用相同点路径创建节点。由于相同路径的节点只能存在一个。所以别的线程去创建已经存在的节点的时候就会失败。这个时候就让线程进入等待,知道该线程代码执行完毕,才释放这个节点,这个时候使用事件监听 的方法。只要监听到节点的删除,就唤醒别的线程来创建这个节点。这里线程的唤醒与等待,使用了信号量的概念。
释放锁
由于创建的是临时节点,所以只要断开客户端的链接,就会使这个节点被删除
释放锁,临时节点。(和会话连接保持一样的状态,只要会话结束,就会删除这个临时节点)。
redis实现分布式锁与Zookeeper实现分布式锁的区别
相同点
在集群的环境下,保证只允许有一个jvm进行执行
从技术上分析
Redis是nosql数据库,主要特点是缓存
Zookeeper是分布式协调工具,主要用户分布式解决方案
实现思路分析
获取锁
Zookeeper,多个客户端(jvm)会在Zookeeper上创建同一个临时节点,因为Zookeeper节点命名路径保证唯一,不允许出现重复,只要谁能创建成功就能获取这个锁
redis,多个jvm会在redis中使用setnx命令创建相同的一个key,因为redis的key保证唯一,不允许重复。只要谁先创建成功,谁就能获取锁
释放锁
Zookeeper直接关闭 临时节点session会话连接,因为临时节点生命周期session会话绑定在一起,如果session会话连接关闭的话,就会使这个临时节点被删除
然后客户端使用事件监听,监听到临时节点被删除,就释放锁
redis释放锁的时候,为了保证是锁的一致性问题,在删除锁的时候需要判断对应的value是否是对应创建的那个业务的id
死锁解决
Zookeeper使用会话有效方式解决死锁的现象
redis是对key设置有效期来解决死锁现象
性能上:
因为redis是nosql,所以redis比Zookeeper性能好
可靠性
Zookeeper更加可靠。因为redis有效求不是很好控制,可能会产生延迟