在分布式系统中,如果多个节点同时操作同一个数据,会造成数据不一致的问题。和多个线程对共享变量进行操作遇到的问题一样。
在java多线程中,一般会对操作共享数据的代码进行加锁,java提供了synchronized关键字可以很方便实现代码加锁。而在分布式系统中,有三种方式实现分布式锁:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁
1. redis分布式锁第一种方式
使用setnx和expire命令实现。
setnx:如果key不存在则设置value并返回1,如果key存在则不进行任何操作返回0
class RedisLock1{
private Jedis jedis;
private String lockKey;
private int expireTime;
{
jedis=new Jedis("localhost");
lockKey="lockKey";
expireTime=2000;
}
/**
* 获取锁(传统方式)
* @return 是否获取到锁
*/
public boolean getLock1(){
String lockValue=String.valueOf(System.currentTimeMillis()+expireTime);
Long result=jedis.setnx(lockKey, lockValue);
// 如果当前锁不存在,返回加锁成功
if(result==1){
return true;
}
//如果当前锁不存在,分以下几种状况:
Long currentLockValue=Long.parseLong(jedis.get(lockKey));
//锁超过了过期时间却仍然存在。造成原因:获取锁后执行逻辑时,程序节点意外崩溃,没有执行到expire或del命令
if(currentLockValue<System.currentTimeMillis()){
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockKey, (System.currentTimeMillis()+expireTime)+"");
Long oldValueLong=Long.parseLong(oldValueStr);
//同一个程序节点,在多线程情况下,这两个值可能不相等
if (oldValueStr != null && oldValueLong==currentLockValue) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
return true;
}
}
//其他情况下,都是没有获取锁
return false;
}
/**
* 解锁(传统方式)
* 存在问题:如果过期时间过短,逻辑还没有执行结束,锁已经过期被删除。如果其他程序节点获取到了锁。这时逻辑执行结束后就会删除其他节点的获取的锁,
*/
public void unLoc1k(){
/**
* 设置过期时间,防止程序崩溃,造成死锁(事实上,有可能程序崩溃,这一步都没执行到,所以需要在获取锁时进行额外判断,判断锁是否已经过期释放。
* 如果锁存在且根据值判断已经过了过期时间, 则表明解锁时程序崩溃,没有执行到expire命令,需要重新设置锁)
*/
jedis.expire(lockKey, expireTime/1000);
//TODO 获取到锁之后需要执行的逻辑(可以采用AOP的方式)
//执行完逻辑,无论锁是否过期删除,都需要手动删除一遍
jedis.del(lockKey);
jedis.close();
}
}
缺点:
- 因为各个节点都设置了过期时间,所以各个节点的时间必须同步
- 不能保证加锁和解锁是同一个节点,有可能会删除其他节点获取的锁
2. redis分布式锁第二种方式
使用 jedis.set(String key, String value, String nxxx, String expx, int time) 实现加锁
class RedisLock2{
private Jedis jedis;
private String lockKey;
private int expireTime;//key过期时间,单位为秒
{
jedis=new Jedis("127.0.0.1");
lockKey="lockKey2";
expireTime=2;
}
/**
* 获取分布式锁
* @param lockValue 锁的值
* @return 是否获取成功
*/
public boolean getLock2(String lockValue) {
/**
* 第一个参数是锁的key
* 第二个参数是锁的value,为了防止解锁时,解了其他成程序节点的锁,因此每个节点获取锁时,lockValue应该唯一,比如使用UUID的值
* 第三个参数设置的是“NX”,意思是SET IF NOT EXIST,即当key不存在时,进行set操作;若key已经存在,则不做任何操作;
* 第四个参数设置的是“PX”,设置过期时间标志,具体值由第五个参数决定
* 第五个参数是锁的过期时间,单位为秒
*/
String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
//锁设置成功,则返回"OK"
if ("OK".equals(result)) {
return true;
}
return false;
}
/**
* 释放分布式锁(通过lua语句)
* 在lua语句中实现锁值比较和删除,一个lua命令是原子操作,不会出现误删其他节点锁的情况
* @param lockValue 锁的值
* @return 是否释放成功
*/
public boolean unLockByLua(String lockValue) {
//TODO 获取到锁之后需要执行的逻辑
Long SUCCESS = 1L;
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(lockValue));
if (SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 节点获取锁执行完逻辑后,通过锁值判断是不是该节点加的锁,如果是则删除锁
* 存在问题:判断和删除不是原子操作,仍有可能判断成功后,删除其他的节点的key,虽然可能性很小
* @param lockValue 传入的锁的值
* @return
*/
public boolean unLock2(String lockValue){
//TODO 获取到锁之后需要执行的逻辑
// 判断加锁与解锁是不是同一个节点
if (lockValue.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
jedis.close();
}
return false;
}
}
2.1测试
这里使用线程模拟分布式的节点。
public class RedisConnection {
private int count=0;
public void incre(){
count++;
System.err.println(count);
}
public static void main(String[] args){
RedisConnection connection=new RedisConnection();
for (int i = 0; i <10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
RedisLock2 lock2=connection.new RedisLock2();
String lockValue=UUID.randomUUID().toString();
if(lock2.getLock2(lockValue)){
//执行逻辑
connection.incre();
lock2.unLockByLua(lockValue);
}else{
System.err.println("没有拥有锁");
try {
int i=10;
while(i>0){
Thread.sleep(10);
i--;
if(lock2.getLock2(lockValue)){
//执行逻辑
connection.incre();
lock2.unLockByLua(lockValue);
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}
执行结果:
1
没有拥有锁
没有拥有锁
没有拥有锁
没有拥有锁
没有拥有锁
2
3
没有拥有锁
4
5
6
7
8
9
10
这里启动了10个线程模拟10个节点,如果节点没有获取到锁,那么休眠10毫秒,再次获取锁,周而复始(10次机会,轮询次数视具体情况而定)
感谢作者!
3.Redisson实现分布式锁
参考: