文章内容输出来源:拉勾教育Java高薪训练营
在单机环境中,当遇到共享资源的时候,为了防止多线程或多进程对共享资源的同时读写访问造成的数据损坏,就需要线程或进程首先从内核/类库获取一把互斥锁,拿到锁的线程或进程就可以排他性的访问共享资源。在分布式环境中,同样需要一个提供同样功能的分布式服务,不同的机器通过该服务获取一把锁,获取到锁的机器就可以排他性的访问共享资源,我们把这样的服务统称为分布式锁服务,锁也就叫分布式锁。下图为单机锁到分布式锁图示:
分布式锁实现方案有:
(1)利用redis自带的分布式锁;
(2)利用redisson框架实现分布式锁;
(3)利用zookeeper实现分布式锁;
在Redis里使用 SET key value [EX seconds] [PX milliseconds] NX
创建一个key,这样就算加锁。其中:
NX
:表示只有 key
不存在的时候才会设置成功,如果此时 redis 中存在这个 key
,那么设置失败,返回 nil
。
EX seconds
:设置 key
的过期时间,精确到秒级。意思是 seconds
秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。
PX milliseconds
:同样是设置 key
的过期时间,精确到毫秒级。
使用设置分布式锁:
//设置专属于自己锁的值
String token = UUID.randomUUID().toString();
String lockResult = jedis.set("TestTable:"+id+":lock",token,"nx","px",10*1000);
if(lockResult!=null&&!"".equals(lockResult)&&"OK".equals(lockResult)){
//设置成功,有权带锁请求A(锁的value="1")在设置的10s内可以访问数据库
}
释放锁就是删除 key ,但是一般可以用 lua
脚本删除,判断 value 一样才删除:
//删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
//用token来确认删除的是自己的锁
if(lockToken!=null&&!"".equals(lockToken)&&lockToken.equals(token)){
//为了防止删除了别人的锁,这里引入lua脚本进行同步删,即获得到value-token的同时进行删除key
String script = "if redis.call('get',KEY[1])==ARGV[1] then return redis.call('del',key[1]) else retrun 0 end";
jedis.eval(script,Collections.singletonList("lock"),Collections.singletonList(token));
jedis.del("TestTable:"+id+":lock");
}
redisson框架实现分布式锁是基于redis进行分布式锁的加锁与释放锁的,从源码中可以看到大量使用了lua代码,即将复杂的业务逻辑,通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。具体详细情况可以参考官网。
redisson官网:https://redisson.org/
基于SpringBoot的redisson使用过程如下:
首先引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.5</version>
</dependency>
然后构建Redisson实例:
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
最后使用:
@Autowired
private RedissonClient redissonClient;
@RequestMapping(value="/test/redisson")
public ResultMsg test(){
String key = "test_key";
RLock rLock = redissonClient.getLock(key);
try{
//加锁
rLock.lock(5,TimeUnit.SECONDS);
//模拟执行任务
Thread.sleep(1000*5);
}catch(Exception e){
e.printStackTrace();
}finally{
//释放锁
rLock.unlock();
}
return ResultMsg.success();
}
zk 分布式锁,就是某个节点尝试创建临时Znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个Znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
其大致过程如下:
获取锁:先获取zookeeper连接,创建znode节点来获取锁,如果创建znode节点成功即获取到锁;如果创建失败,则注册一个监听器监听这个锁,若这个锁释放了,它则会创建自己的节点获取锁;
释放锁:就是删除这个节点znode;
/**
* ZooKeeperSession
*/
public class ZooKeeperSession {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private ZooKeeper zookeeper;
private CountDownLatch latch;
public ZooKeeperSession() {
try {
this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());
try {
connectedSemaphore.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ZooKeeper session established......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取分布式锁
*
* @param productId
*/
public Boolean acquireDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception e) {
while (true) {
try {
// 相当于是给node注册一个监听器,去看看这个监听器是否存在
Stat stat = zk.exists(path, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception ee) {
continue;
}
}
}
return true;
}
/**
* 释放掉一个分布式锁
*
* @param productId
*/
public void releaseDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.delete(path, -1);
System.out.println("release the lock for product[id=" + productId + "]......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 建立 zk session 的 watcher
*/
private class ZooKeeperWatcher implements Watcher {
public void process(WatchedEvent event) {
System.out.println("Receive watched event: " + event.getState());
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
if (this.latch != null) {
this.latch.countDown();
}
}
}
/**
* 封装单例的静态内部类
*/
private static class Singleton {
private static ZooKeeperSession instance;
static {
instance = new ZooKeeperSession();
}
public static ZooKeeperSession getInstance() {
return instance;
}
}
/**
* 获取单例
*
* @return
*/
public static ZooKeeperSession getInstance() {
return Singleton.getInstance();
}
/**
* 初始化单例的便捷方法
*/
public static void init() {
getInstance();
}
}
(1)获取锁方面
redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
zookeeper分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
(2)释放锁方面
如果是redis获取锁的那个客户端出现故障挂了,那么只能等待超时时间之后才能释放锁;
如果zookeeper节点出现故障挂了的话,因为创建的是临时节点Znode,只要客户端挂了,Znode节点就没了,此时就自动释放锁。
综上所述,使用zookeeper实现的分布式锁比redis实现的分布式锁牢靠、而且模型简单易用,注意一般的生产系统中,也可以用redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。
文章内容输出来源:拉勾教育Java高薪训练营
若有错误之处,欢迎留言指正~~~