场景介绍:一个支付订单的微服务根据订单状态查询订单,查询到一个订单状态为1(待支付),然后用户未完成支付,一个取消超时订单的微服务此时根据订单状态也查询到这个订单,随着时间的推移,用户在某个时刻完成了订单支付,并将订单状态修改为2(已支付),过了超时时间,取消超时订单的微服务将这个订单的状态修改为5(取消订单),这个时候,就出现了订单状态不一致的情况,这个时候就需要使用分布式锁来解决这个问题。
可以使用redis或者zookeeper来实现分布式锁,redis相对于可用性强,效率快,zookeeper相对于保持数据一致性强,在redis集群中,如果分布式锁是在master上的,如果master在宕机之前没有同步锁,就有可能出现没有分布式锁的问题,redis是单线程的执行命令,sexnx name zhangsan,如果name存在就不会创建name,并返回0,如果不存在创建name并返回1,那么可以通过谁创建的name,代表谁拿到这个分布式锁,创建name的微服务拿到锁,执行完业务之后,先判断这个key的锁是不是自己加的,如果是,就删除这个key,这是基本思路。在实际生产情况下,拿到锁的微服务如果宕机了,锁没有释放,怎么办,可以给key设置过期时间;如果拿到锁的微服务在拿到锁的基础上又加了锁,就要使用可重入锁;锁释放之后,要保证实现请求的先后顺序拿到锁,要实现公平锁;锁没有释放,一个请求去拿锁,如果没有拿到,可以使用自旋,就是每隔一段时间,访问一下redis,判断锁有没有释放,如果不想自旋,要设置时间,过了时间,没拿到锁,就做别的事去;综上所述,如果自己实现分布式锁要考虑很多问题,要么有框架已经实现了分布式锁,我们直接去用就可以了。Redission就是一个实现了分布式锁的框架,基于redis来实现分布式锁,上面说的几个方面,它都有实现,超时机制、自动续约机制,阻塞加锁和非阻塞加锁(去加锁发现锁已经加了,不一直等待锁的释放)、可重入锁、公平锁、非公平锁等
Map:Map存储在redis中,多个微服务可以操作同一个Map。还有List等
分布式锁的工作原理
它加锁用的是redis中的hash类型,hset 大key 小key value,大key是锁,小key是客户端ID,value是加锁次数(实现可重入锁的功能), 那加锁的时候,要去判断有没有这个锁等,去判断的过程中,如果redis中的数据被修改了怎么办,这就要加锁再去判断,它把这多个命令放在一个lua脚本当中,这个lua脚本可以保证里面的所有命令执行是原子性的,不是这个脚本里面的命令,要么在这个脚本执行之前或者之后执行;如果拿到锁的服务宕机了,锁没有释放怎么办,redis中锁的默认失效时间是30秒,如果30秒之后,拿到锁的微服务的业务代码还没有执行完,redission有一个自动续约功能,通过watchdog机制来实现,只要微服务加锁成功了,它会启动后台的一个调度线程,这个线程会每隔10秒把失效时间重置为30秒,如果加锁的redis节点宕机了,那么watchdog也会释放,那么30秒之后,这个锁也会释放。redission适配redis的各种架构的集群,解锁也是用lua脚本来实现,因为解锁也需要多个命令,要保证这多个命令的原子性。当value减为零的时候,就把redis中这个key删除。
Redission中提供了一个RLock接口,这个接口实现了jdk中的Lock接口
/**
* Copyright (c) 2013-2019 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.api;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* Distributed implementation of {@link java.util.concurrent.locks.Lock}
* Implements reentrant lock.
* Use {@link RLock#getHoldCount()} to get a holds count.
*
* @author Nikita Koksharov
*
*/
public interface RLock extends Lock, RLockAsync {
/**
* Returns name of object
*
* @return name - name of object
*/
String getName();
/**
* Acquires the lock.
*
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until the
* lock has been acquired.
*
* If the lock is acquired, it is held until <code>unlock</code> is invoked,
* or until leaseTime have passed
* since the lock was granted - whichever comes first.
*
* @param leaseTime the maximum time to hold the lock after granting it,
* before automatically releasing it if it hasn't already been released by invoking <code>unlock</code>.
* If leaseTime is -1, hold the lock until explicitly unlocked.
* @param unit the time unit of the {@code leaseTime} argument
* @throws InterruptedException - if the thread is interrupted before or during this method.
*/
void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* Returns <code>true</code> as soon as the lock is acquired.
* If the lock is currently held by another thread in this or any
* other process in the distributed system this method keeps trying
* to acquire the lock for up to <code>waitTime</code> before
* giving up and returning <code>false</code>. If the lock is acquired,
* it is held until <code>unlock</code> is invoked, or until <code>leaseTime</code>
* have passed since the lock was granted - whichever comes first.
*
* @param waitTime the maximum time to aquire the lock
* @param leaseTime lease time
* @param unit time unit
* @return <code>true</code> if lock has been successfully acquired
* @throws InterruptedException - if the thread is interrupted before or during this method.
*/
//如果加上锁,返回true,否则返回false,非阻塞加锁,可以自己设置锁的失效时间,如果自己设置了失效时间,watchdog将会失效
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* Acquires the lock.
*
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until the
* lock has been acquired.
*
* If the lock is acquired, it is held until <code>unlock</code> is invoked,
* or until leaseTime milliseconds have passed
* since the lock was granted - whichever comes first.
*
* @param leaseTime the maximum time to hold the lock after granting it,
* before automatically releasing it if it hasn't already been released by invoking <code>unlock</code>.
* If leaseTime is -1, hold the lock until explicitly unlocked.
* @param unit the time unit of the {@code leaseTime} argument
*
*/
//如果没有加上锁,会自旋
void lock(long leaseTime, TimeUnit unit);
/**
* Unlocks lock independently of state
*
* @return <code>true</code> if lock existed and now unlocked otherwise <code>false</code>
*/
boolean forceUnlock();
/**
* Checks if this lock locked by any thread
*
* @return <code>true</code> if locked otherwise <code>false</code>
*/
boolean isLocked();
/**
* Checks if this lock is held by the current thread
*
* @param threadId Thread ID of locking thread
* @return <code>true</code> if held by given thread
* otherwise <code>false</code>
*/
boolean isHeldByThread(long threadId);
/**
* Checks if this lock is held by the current thread
*
* @return <code>true</code> if held by current thread
* otherwise <code>false</code>
*/
boolean isHeldByCurrentThread();
/**
* Number of holds on this lock by the current thread
*
* @return holds or <code>0</code> if this lock is not held by current thread
*/
int getHoldCount();
/**
* Remaining time to live of this lock
*
* @return time in milliseconds
* -2 if the lock does not exist.
* -1 if the lock exists but has no associated expire.
*/
long remainTimeToLive();
}
redission依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
package com.itheima.itheimadistributelock.core.redis;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
/**
* @author Administrator
*/
public class RedssionLock {
//zookeeper 分布锁
public static RLock getLock() {
Config config = new Config();
//指定使用集群部署方式
//config.useClusterServers();
//指定使用单节点部署方式
config.useSingleServer().setAddress("redis://localhost:6379");//.setPassword("mkxiaoer");
// config.useSingleServer().setPassword("root");
config.useSingleServer().setConnectionPoolSize(500);//设置对于master节点的连接池中连接数最大为500
config.useSingleServer().setIdleConnectionTimeout(10000);//如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
config.useSingleServer().setConnectTimeout(30000);//同任何节点建立连接时的等待超时。时间单位是毫秒。
config.useSingleServer().setTimeout(3000);//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
config.useSingleServer().setPingTimeout(30000);
//获取RedissonClient对象
RedissonClient redisson = Redisson.create(config);
//获取锁对象
RLock rLock = redisson.getLock("lock.lock");
//获取公平锁
//rLock = redisson.getFairLock("lock.lock");
return rLock;
}
/**
* 演示可重入锁
* @param args
*/
public static void main(String[] args) {
RLock lock = RedssionLock.getLock();
lock.lock();
System.out.println("获取锁成功-----1次");
lock.lock();
System.out.println("获取锁成功-----2次");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 整个方法解锁
lock.unlock();
}
}
可以看到TTL的变化,有自动续约功能。
案例测试
package com.itheima.itheimadistributelock;
import com.itheima.itheimadistributelock.config.SellConstants;
import com.itheima.itheimadistributelock.service.GoodsService;
import com.itheima.itheimadistributelock.service.impl.GoodsRedissionLockSerivice;
import com.itheima.itheimadistributelock.service.impl.GoodsServiceImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ItheimaDistributelockApplicationTests implements ApplicationContextAware {
private ApplicationContext applicationContext;
long timed = 0;
@Before
public void start(){
timed = System.currentTimeMillis();
System.out.println("开始测试....");
}
@After
public void end(){
System.out.println("结束测试,执行时长是:" + (System.currentTimeMillis() - timed) / 1000 );
System.out.println("共售出:"+ SellConstants.sellNum.get() + "台产品");
}
@Test
public void buy(){
//模拟请求数量
int serviceNum =1;//4台tomcat 107 班 100
int requesetSize = 100;//每台服务多少并发进入到系统
//倒计数器。用于模拟高并发 juc CountDownLatch 主线分布式锁,线程的阻塞和唤醒jdk5 juc编程提供并发编程类
CountDownLatch countDownLatch = new CountDownLatch(1);
//循环创建N个线程
List<Thread> threads = new ArrayList<>();
String userId = "100",goodsId = "apple";
int stock = 2;
//模拟服务器的数量
for (int i = 0; i < serviceNum; i++) {
// 未使用锁
GoodsService goodsService = applicationContext.getBean(GoodsServiceImpl.class);
// 使用 synchronized 锁
// GoodsService goodsService = applicationContext.getBean(GoodsSyncSerivice.class);
// 使用 zookeeper 实现分布式锁
// GoodsService goodsService = applicationContext.getBean(GoodsZkLockSerivice.class);
// 使用 Jedis ==> Redis 实现分布式锁
// GoodsService goodsService = applicationContext.getBean(GoodsJedisLockService.class);
// 使用 Redission ==> Redis 实现分布式锁
//GoodsService goodsService = applicationContext.getBean(GoodsRedissionLockSerivice.class);
//模拟每台服务器发起请求的数量
for (int i1 = 0; i1 < requesetSize; i1++) {
Thread thread = new Thread(()->{
try {
//等待countdownlatch值为0,也就是其他线程就绪后,在运行后续的代码。
countDownLatch.await();
//执行吃饭的动作
goodsService.buy(userId,goodsId,stock);
}catch (Exception ex){
throw new RuntimeException(ex);
}
});
//添加线程到集合中
threads.add(thread);
//启动线程
thread.start();
}
}
//并发执行所有请求
countDownLatch.countDown();
threads.forEach((e)->{
try {
e.join();
}catch (Exception ex){
throw new RuntimeException(ex);
}
});
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
数据库中apple的数量
运行完代码之后查看数据库
注释掉
// 未使用锁 GoodsService goodsService = applicationContext.getBean(GoodsServiceImpl.class);
打开
// 使用 Redission ==> Redis 实现分布式锁 GoodsService goodsService = applicationContext.getBean(GoodsRedissionLockSerivice.class);
恢复数据库中apple的数量为100
运行完代码查看数据库
说明项目有bug,有时候正常,有时候不正常