前言
一:发展由来
大多数互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提升,但为此,我们就需要多解决一个分布式环境下,数据一致性的问题。
当某个资源在多系统之间,具有共享性的时候,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写有人读,大家访问到的数据就不一致了。
二:我们为什么需要分布式锁?
在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
典型的冲突有:
1.丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
2.脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等), 在同一个jvm进程中时,可以使用JUC提供的一些锁来解决多个线程竞争同一个共享资源时候的线程安全问题,但是当多个不同机器上的不同jvm进程共同竞争同一个共享资源时候,juc包的锁就无能无力了,这时候就需要分布式锁了。
但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。
因此,为了解决这个问题,我们就必须引入「分布式锁」。
分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
分布式锁要满足哪些要求呢?
排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取
避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
常见的有使用redis的setNX
函数,乐观锁数据库version版本
来实现,ZooKeeper锁
,本节我们谈谈使用zookeeper的临时序列节点机制来实现一个分布式锁。
三:各种锁的对比
1Synchronized 修身的Java方法,其实就是Synchronized对 this或类(静态类) 的锁定,解决资源竞争问题 性能最低,尽量少用
临界区:通过对多线程的串行化来访问公共资源或一段代码
2互斥量:采用互斥对象机制。只有拥有互斥对象的线程才能够访问公共资源的权限
Synchronized 修身的代码代码块 ,单台服务器可使用。
3分布式锁的实现技术数据库version版本号
基于数据库实现分布式锁 多采用乐观锁实现。尽量少用
性能较差,容易出现单点故障
锁没有失效的时间,容易死锁
非阻塞式的
4基于缓存实现分布式锁 小系统多用,Redis实现原理:保存Redis数据key表示添加了锁,设置失效时间,删除key表示解锁。
非阻塞式的
5基于Zookeeper实现分布式锁
实现比较简单、可靠性高、性能较好
四:开始实践zookeeper的临时序列节点机制实现
启动zk服务端
pom依赖
<!--zookeeper-->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
自定义锁接口
/**
* 我的分布式锁接口
*/
public interface ILock {
//获取锁
void Lock();
//释放锁
void UnLock();
}
抽象类实现该锁接口
import com.al.lock.service.ILock;
import org.I0Itec.zkclient.ZkClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/***
* 实现Ilock锁 连接zk 创建临时节点
*/
public abstract class ZKAbstractLock implements ILock {
private static Logger log=LoggerFactory.getLogger(ZKAbstractLock.class);
private static String host="localhost";
private static String port="2181";
//分布式锁,通过创建统一的临时节点 创建成功则表示获取锁成功,否则失败
protected static String node="/zklock";
protected ZkClient client=new ZkClient(host+":"+port);
/**
* 1:先试着创建临时节点,创建成功则获取锁
* 2:如果创建失败,表示已被其他线程上锁了;需要监视这个节点删除(其他线程释放锁),并且使用CountDownLatch 休眠当前线程
* 3:当其他线程释放锁后,唤醒当前线程,重新获取锁。
* 缺点:有多个线程等待这个锁时,一个线程释放锁后,其他线程都会被唤醒进行锁的获取(只有一个会成功获取),
* 这样导致竞争激烈,资源浪费。
* 解决思路,使用临时顺序节点,当有锁后只有一个线程监视这个节点,其他线程不监视。这个线程释放锁后通知下一个线程获取锁
*/
public void Lock() {
if(tryLock()){
log.info(Thread.currentThread().getName()+ " Get Lock Success!!!");
}else{
//等待之后重新获取锁
waitforLock();
Lock();
}
}
//释放当前锁,由于创建的是临时节点,则关闭zk的连接,锁自动释放
public void UnLock() {
client.close();
}
//试着去获取锁
protected abstract boolean tryLock();
//等待获取锁
protected abstract void waitforLock();
}
继承抽象类完成具体的实现
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.exception.ZkException;
import java.util.concurrent.CountDownLatch;
public class ZKLockImp extends ZKAbstractLock {
private CountDownLatch cld = null;
/**
* 创建临时节点,创建成功则说明获取锁,否则表示获取锁失败
*/
@Override
protected boolean tryLock() {
try {
client.createEphemeral(node);
return true;
}catch (ZkException e) {
return false;
}
}
/**
* 获取锁失败后,需要在这里让线程休眠等待
*/
@Override
protected void waitforLock() {
//对ZK创建一个节点监视器 watcher
IZkDataListener listener = new IZkDataListener() {
//当zk临时节点删除后触发。当其他线程释放锁后,这个临时节点会被删除,从而触发
//让CountDownLatch 减一,从而唤醒线程
public void handleDataDeleted(String dataPath) throws Exception {
if (cld != null) {
cld.countDown();
}
}
//当节点值改变后触发
public void handleDataChange(String dataPath, Object data)
throws Exception {
}
};
//对节点添加监视器
client.subscribeDataChanges(node, listener);
//节点存在表示之前有锁已经被占用,让线程等待这里
if (client.exists(node)) {
cld = new CountDownLatch(1);
try {
cld.await();//(异步等待,定减为0)线程会在这里堵塞,指定门闩指数为0
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//对节点移除监视器
client.unsubscribeDataChanges(node, listener);
}
}
分布式锁工厂类
/***
* 分布式锁生产工厂
*/
public class OrderFactory {
private static Integer i=0;
public static String GetOrder(){
//JDK 锁
// synchronized (i) {
// i++;
// return "NewOrder"+new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-"+i).format(new Date());
// }
//分布式锁
i++;
String ss= "NewOrder"+new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-"+i).format(new Date());
return ss;
}
}
使用CountDownLatch测试生产分布式锁唯一订单编号
import com.al.lock.service.ILock;
import com.al.lock.service.impl.OrderFactory;
import com.al.lock.service.impl.ZKLockImp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch;
/***
* 持久节点(PERSISTENT ):节点创建后,一直存在,直到主动删除了该节点。
* - 临时节点(EPHEMERAL) :生命周期和客户端会话绑定,一旦客户端会话失效,这个节点就会自动删除。
* - 序列节点(SEQUENTIAL ):多个线程创建同一个顺序节点时候,每个线程会得到一个带有编号的节点,节点编号是递增不重复的
*/
/***
* 使用了zk节点唯一性来分布式保证高并发锁
* 缺点:如果使用的临时节点,那么如果一旦当前节点释放锁后,其余线程都会同时类访问当前锁,就会导致内存消耗,性能下降
* 解决:使用zk临时顺序节点,当前节点获取了锁,只有后面一个线程进行等待,其余线程无需等待,这样就大大提高了程序的性能。
*/
public class OrderIDTest implements Runnable {
private static int count = 100;//并发线程数量
private static CountDownLatch cdl = new CountDownLatch(count);
ILock lock = new ZKLockImp();
//启动线程
public void run() {
createOrderNum();
}
//创建订单
public void createOrderNum() {
lock.Lock();//创建临时节点上锁,其他线程等待
String orderNum = OrderFactory.GetOrder();//开始生产订单id
System.out.println(Thread.currentThread().getName() + "创建了订单号:【" + orderNum+ "】!");
lock.UnLock();//生产完毕释放当前锁
}
//1000个并发模拟生成唯一订单ID
public static void main(String[] args) {
for (int i = 0; i < count; i++) {
new Thread(new OrderIDTest()).start();
//发令枪里面的数字减一
cdl.countDown();
}
}
}
使用压测工具 jmeter测试生产分布式锁唯一订单编号
五:基于乐观锁数据库version版本号控制
简介:
我们先来看一下如何基于「乐观锁」来实现:
乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。
当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。
如图,假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。但如果这里用到乐观锁机制,当两个用户去数据库中读取余额
的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。
通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足:
(1)锁服务要有递增的版本号version
(2)每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号
update task
set key =value,version=version+1
where id=#{id} and version=#{version}; //这条语句,就可以更新数据成功啦~
六:基于redis缓存来实现分布式锁
简介:
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。
SETNX命令简介
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。
返回整数,具体为
- 1,当 key 的值被设置
- 0,当 key 的值没被设置
expire命令简介
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
delete命令简介
delete key
删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
实践:使用的是jedis来连接Redis
- 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
- 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
- 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
pom.xml
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
redis.properties
server.port=8098
#redis jedis配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=-1
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
#spring-session 使用
spring.session.store-type=none
RedisConfig初始化Jedis连接
@Slf4j
@Configuration
@PropertySource("classpath:redis.properties")
public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.pool.max-wait}")
private long maxWaitMillis;
@Value("${spring.redis.password}")
private String password;
@Bean
public JedisPool redisPoolFactory(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,timeout,password);
log.info("JedisPool注入成功!");
log.info("redis地址:" + host + ":" + port);
return jedisPool;
}
}
定义锁的接口
/***
* 我的锁实现
*/
public interface LockService {
/***
* 获得锁并且设置锁失效时间
* @param locaName 锁的key
* @param acquireTimeout 获取超时时间
* @param timeout 锁的超时时间
* @return 锁标识
*/
String lockWithTimeout(String locaName, long acquireTimeout, long timeout);
/***
* 根据锁的名称删除这个锁
* @param lockName 锁的key
* @param identifier 释放锁的标识
* @return 是否成功
*/
boolean releaseLock(String lockName, String identifier) ;
}
实现该接口进行
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;
import java.util.List;
import java.util.UUID;
public class LockServiceImpl implements LockService{
private final JedisPool jedisPool;
public LockServiceImpl(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加锁
* @param locaName 锁的key
* @param acquireTimeout 获取超时时间
* @param timeout 锁的超时时间
* @return 锁标识
*/
public String lockWithTimeout(String locaName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 获取连接
conn = jedisPool.getResource();
// 随机生成一个value
String identifier = UUID.randomUUID().toString();
// 锁名,即key值
String lockKey = "lock:" + locaName;
// 超时时间,上锁后超过此时间则自动释放锁
int lockExpire = (int)(timeout / 1000);
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值,用于释放锁时间确认
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key没有设置超时时间,为key设置一个超时时间
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 释放锁
* @param lockName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 监视lock,准备开始事务
conn.watch(lockKey);
// 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
if (!StringUtils.isEmpty(conn.get(lockKey)) && identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
//e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
分布式锁工厂类
/***
* 分布式锁生产工厂
*/
public class OrderFactory {
private static Integer i=0;
public static String GetOrder(){
//JDK 锁
// synchronized (i) {
// i++;
// return "NewOrder"+new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-"+i).format(new Date());
// }
//分布式锁
i++;
String ss= "NewOrder"+new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-"+i).format(new Date());
return ss;
}
}
LockOrderController测试
import com.al.common.OrderFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import redis.clients.jedis.JedisPool;
@Controller
public class LockOrderController {
@Autowired
private JedisPool jedisPool;
//创建订单
@RequestMapping("/createOrderNum")
@ResponseBody
public String createOrderNum() {
LockServiceImpl lock = new LockServiceImpl(jedisPool);
// 返回锁的value值,供释放锁时候进行判断
String indentifier = lock.lockWithTimeout("resource", 5000, 100);//设置锁,加时间
String orderNum = OrderFactory.GetOrder();//开始生产订单id
System.out.println(Thread.currentThread().getName() + "创建了订单号:【" + orderNum+ "】!");
lock.releaseLock("resource", indentifier);//生产完毕释放当前锁
return orderNum;
}
}
jmeter 模拟测试
需要源码的联系博主,或者留言私发…
以上总结全部是个人和网上经验总结,如有雷同,请谅解,欢迎大家研讨技术,点关注,后续继续更新…