分布式锁的实现方式

简介

  • 在单机环境中,java提供了很多并发的API,但是这些API仅限于同一节点上的应用,对分布式场景无能为力。
  • 要求:
    • 一把锁在同一集群中只能被一个节点上的一个线程获得,且被该线程释放
    • 可重入,阻塞锁。
    • 获取和释放锁高可用(集群,超时)、高效。
  • 较常用的几种方案:
    • 基于数据库实现(简单)。
    • 基于redis等缓存实现(高效)。
    • 基于zookeeper实现(可靠)。

mysql

实现

  • 利用数据库的行锁和唯一键保证锁只会被一个线程获取。
    • 一条记录作为一个锁,主键作为锁的Key。
    • insert获取锁,delete释放锁。参考
CREATE TABLE methodLock (
  id varchar(64) NOT NULL COMMENT '主键',
  update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (id),
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

特点

  • 优点:
    • 实现简单。
  • 缺点:
    • 没有失效时间(可以通过定时任务清数据解决)。
    • 非阻塞(while重试)。
    • 非重入,且无法保证是持有锁的线程删除锁(增加字段记录节点和线程信息,insert之前先select)。
    • 操作数据库,性能不高。

原理

  • 实现原理:mysql自身的行锁。

redis

实现

  • redis基本使用
  • 利用redis的单线程保证只有一个线程获取锁。
    • key-value作为锁。
    • set获取锁:requestId标识进程,NX只在key不存在时设置,PX和expireTime设置毫秒级的超时时间。
    • del删除锁:校验是当前进程的锁之后再删除。lua脚本解锁
    • jedis 非线程安全 ,需要线程持有自己的对象作为入参。多线程异常多线程中使用jedis
@Service
public class RedisLockServiceImpl {
    @Autowired
    private JedisPool jedisPool;

    public Jedis getJedis() {
        return jedisPool.getResource();
    }

    public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
        return "OK".equals(result);
    }

    public boolean unlock(Jedis jedis, String lockKey, String requestId) {
        Long result = null;
        if (requestId.equals(jedis.get(lockKey))) {
            result = jedis.del(lockKey);
        }
        return Objects.equals(result, 1L);
    }
}

特点

  • 优点:
    • 高性能,实现较简单。
    • 可设置失效时间。
    • 校验value保证是持有锁的线程释放锁。
  • 缺点:
    • 非阻塞(while重试)。
    • 非重入(set之前查询value是否为对应的线程)。

原理

  • 实现原理:redis文件事件分派器的单线程。

zookeeper

实现

@Service
public class ZkLockServiceImpl {

    private Logger logger = LoggerFactory.getLogger(ZkLockServiceImpl.class);

    private CuratorFramework client;

    private String zkAddress = "xx.xx.xx.xx:2181";

    private Map<String, InterProcessMutex> interProcessMutexMap = Maps.newConcurrentMap();

    @PostConstruct
    private void init() {
        try {
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(10000, 6);
            client = CuratorFrameworkFactory.newClient(zkAddress, 10000, 180000, retryPolicy);
            client.start();
			//可重入
            InterProcessMutex mutex = new InterProcessMutex(client, LockConst.ZK_TEST_LOCK);
            interProcessMutexMap.put(LockConst.ZK_TEST_LOCK, mutex);
        } catch (Exception e) {
            logger.error("ZkLockServiceImpl init failed! e : {}", e);
        }
    }

    public boolean tryLock(String path) {
        InterProcessMutex mutex = interProcessMutexMap.get(path);
        try {
            return mutex.acquire(2000L, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            logger.error("ZkLockServiceImpl tryLock error! e : {}", e);
            return false;
        }
    }

    public boolean unlock(String path) {
        try {
            InterProcessMutex mutex = interProcessMutexMap.get(path);
            if (mutex.isAcquiredInThisProcess()) {
                mutex.release();
            } else {
                logger.warn(Thread.currentThread().getName() + ": not in this process");
            }
        } catch (Exception e) {
            logger.error("ZkLockServiceImpl release error! e : {}", e);
            return false;
        }
        return true;
    }
}

特点

  • 优点:
    • 可设置失效时间,并且节点挂掉之后会删除该临时节点。
    • isAcquiredInThisProcess保证是持有锁的线程释放锁。
    • InterProcessMutex实现重入,InterProcessSemaphoreMutex实现非重入。
  • 缺点:
    • 需要创建和销毁节点,性能不够高。
    • 非阻塞(while重试)。

原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值