0%

分布式锁

分布式锁

分布式锁的实现一般有redis或zookeeper临时顺序节点以及基于数据库行锁来实现

redis分布式锁

利用redis的setnx命令,只有在key不存在的情况下,才能set成功

1
2
3
4
5
6
7
8
9
10
11
12
13
//加锁  返回1,说明key原本不存在,线程得到了锁;返回0说明key已经存在了,获取锁失败
jedis.setnx(key,value)

// 释放锁,将key删掉,这样setnx就可以获得锁了
jedis.del(key)

// 锁超时 当超过该时间,则会自动释放锁
jedis.expire(key, 30)


if (jedis.setnx(lockKey, val) == 1) {
jedis.expire(lockKey, timeout);
}

但是setnx+expire是两个操作,不是原子性的,可能会出现setnx刚执行成功,还没来得及执行expire命令,服务挂掉了,这样就会使得该key没有设置过期时间,该key会一直存在,导致别的服务永远无法获取到锁

1
2
3
4
// 可以使用set方法  
//nxxx参数是NX|XX,如果为NX,则表示只有不存在该key的时候才会set;XX表示只有存在该key的时候才会set
// expx参数是EX|PX EX表示过期时间单位是秒,PX表示过期时间单位是毫秒
String set(String key, String value, String nxxx, String expx, long time);

RedisTemplate中没有这种方法,不过可以使用execute来调用原生的jedis来操作set命令

1
2
3
4
5
6
7
String result = redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(key, "锁定的资源", "NX", "PX", expire);
}
});

但是有可能会出现另一种情况,客户端A获取到锁之后,开始执行业务,此时key过期删除了,客户端B同样获取到锁,之后客户端A执行完之后就会删除锁,此时删除的是客户端B的锁,所以在加锁的时候可以使用随机字符串requestId作为value,删除之前进行验证key对应的value是不是自己的requestId

1
2
3
4
// 为保证操作原子性,使用lua脚本
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(requestId));

redission框架

redission中已经实现了redis分布式锁

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
// 1. Create config object
Config config = new Config();
config.useClusterServers()
// use "rediss://" for SSL connection
.addNodeAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
// 加锁
lock.lock();
// 解锁
lock.unlock();

redission中的指令都是使用lua脚本执行的

zookeeper分布式锁

利用zookeeper的顺序临时节点,在zookeeper中创建一个临时顺序节点locks

  • 三个系统A/B/C都去访问locks节点,访问的时候会创建带顺序号的临时/短暂节点,(例如,A创建了id_0000节点,B创建了id_0001节点,C创建了id_0002节点)
  • 拿到/locks节点下的所有子节点(id_0000,id_0001,id_0002),getChildren方法,判断自己创建的是不是最小的节点
  • 如果是,则拿到锁,执行完操作之后,把创建的节点删掉,即为释放锁
  • 如果不是,找到比自己小1的节点,exists方法,并进行监听,发现比自己小1的节点删掉之后,发现自己已经是最小的节点了,拿到锁
  • 如果在操作过程中,应用服务挂掉了,那么由于连接断开,此时的顺序临时节点也会自动删除

zookeeper的分布式锁由于需要创建、销毁临时节点来实现锁功能,所以性能不太高。在并发量很大的情况下不建议使用

Curator框架

Curator实现了zookeeper的分布式锁实现

1
2
3
4
5
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.4.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3)
CuratorFramework client = CuratorFrameworkFactory.newClient(zookeeperConnectionString, retryPolicy);
client.start();

InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) )
{
try
{
// do some work inside of the critical section here
}
finally
{
lock.release();
}
}

基于数据库

mysql数据库的innodb本身是支持行锁的,使用select xxx for update可以来支持分布式锁

也可以创建一个lock表,来设置锁的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 加锁
INSERT INTO shedlock
(name, lock_until, locked_at, locked_by)
VALUES
(锁名字, 当前时间+最多锁多久, 当前时间, 主机名)

// 续约
UPDATE shedlock
SET lock_until = 当前时间+最多锁多久,
locked_at = 当前时间,
locked_by = 主机名 WHERE name = 锁名字 AND lock_until <= 当前时间

// 释放锁
UPDATE shedlock
SET lock_until = lockTime WHERE name = 锁名字

使用mysql不适合于高并发场景

欢迎关注我的其它发布渠道