分布式锁实现原理与最佳实践

2025 年国人友好神刊盘点:20 本审稿快、分区高的 SCI 期刊,速抢发文先机! 科睿唯安最新发布的 2024 年影响因子(2025 年公布)显示,多本 TOP 期刊影响因子迎来大幅上涨!尤其是对国人友好的期刊,因审稿效率高、版面费亲民、国人发文占比超 40%,成为科研人员投稿热门。本文精选 20 本潜力神刊,涵盖Cell 子刊、顶流期刊、黑马期刊、性价比之王及新晋分区 TOP,附完整的影响因子、中科院分区、审稿周期、收稿方向攻略,助你抢占发文先机! 阅读详情

作者:秦泽涛 阿里云教育基座团队

在单体的应用开发场景中涉及并发同步时,大家往往采用 Synchronized(同步)或同一个 JVM 内 Lock 机制来解决多线程间的同步问题。而在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁。接下来本文将为大家分享分布式锁的最佳实践。

一、超卖问题复现

1.1 现象

存在如下的几张表:

  • 商品表

  • 订单表

  • 订单 item 表

商品的库存为 1,但是并发高的时候有多笔订单。

错误案例一:数据库 update 相互覆盖

直接在内存中判断是否有库存,计算扣减之后的值更新数据库,并发的情况下会导致相互覆盖发生:

@Transactional(rollbackFor = Exception.class)public Long createOrder() throws Exception {    Product product = productMapper.selectByPrimaryKey(purchaseProductId);    // ... 忽略校验逻辑​    //商品当前库存    Integer currentCount = product.getCount();    //校验库存    if (purchaseProductNum > currentCount) {        throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");    }    // 计算剩余库存    Integer leftCount = currentCount - purchaseProductNum;    // 更新库存    product.setCount(leftCount);    product.setGmtModified(new Date());    productMapper.updateByPrimaryKeySelective(product);​    Order order = new Order();    // ... 省略 Set    orderMapper.insertSelective(order);​    OrderItem orderItem = new OrderItem();    orderItem.setOrderId(order.getId());    // ... 省略 Set    return order.getId();}

复制代码

错误案例二:扣减串行执行,但是库存被扣减为负数

在 SQL 中加入运算避免值的相互覆盖,但是库存的数量变为负数,因为校验库存是否足够还是在内存中执行的,并发情况下都会读到有库存:

@Transactional(rollbackFor = Exception.class)public Long createOrder() throws Exception {    Product product = productMapper.selectByPrimaryKey(purchaseProductId);    // ... 忽略校验逻辑​    //商品当前库存    Integer currentCount = product.getCount();    //校验库存    if (purchaseProductNum > currentCount) {        throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");    }    // 使用 set count =  count - #{purchaseProductNum,jdbcType=INTEGER}, 更新库存    productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());    Order order = new Order();    // ... 省略 Set    orderMapper.insertSelective(order);​    OrderItem orderItem = new OrderItem();    orderItem.setOrderId(order.getId());    // ... 省略 Set    return order.getId();}

复制代码

错误案例三:使用 synchronized 实现内存中串行校验,但是依旧扣减为负数

因为我们使用的是事务的注解,synchronized 加在方法上,方法执行结束的时候锁就会释放,此时的事务还没有提交,另一个线程拿到这把锁之后就会有一次扣减,导致负数。

@Transactional(rollbackFor = Exception.class)public synchronized Long createOrder() throws Exception {    Product product = productMapper.selectByPrimaryKey(purchaseProductId);    // ... 忽略校验逻辑​    //商品当前库存    Integer currentCount = product.getCount();    //校验库存    if (purchaseProductNum > currentCount) {        throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");    }    // 使用 set count =  count - #{purchaseProductNum,jdbcType=INTEGER}, 更新库存    productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());    Order order = new Order();    // ... 省略 Set    orderMapper.insertSelective(order);​    OrderItem orderItem = new OrderItem();    orderItem.setOrderId(order.getId());    // ... 省略 Set    return order.getId();}

复制代码

1.2 解决办法

从上面造成问题的原因来看,只要是扣减库存的动作,不是原子性的。多个线程同时操作就会有问题。

  • 单体应用:使用本地锁 + 数据库中的行锁解决

  • 分布式应用:

  • 使用数据库中的乐观锁,加一个 version 字段,利用 CAS 来实现,会导致大量的 update 失败

  • 使用数据库维护一张锁的表 + 悲观锁 select,使用 select for update 实现

  • 使用Redis 的 setNX实现分布式锁

  • 使用zookeeper的watcher + 有序临时节点来实现可阻塞的分布式锁

  • 使用Redisson框架内的分布式锁来实现

  • 使用curator 框架内的分布式锁来实现

二、单体应用解决超卖的问题

正确示例:将事务包含在锁的控制范围内

保证在锁释放之前,事务已经提交。

//@Transactional(rollbackFor = Exception.class)public synchronized Long createOrder() throws Exception {    TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);    Product product = productMapper.selectByPrimaryKey(purchaseProductId);    if (product == null) {        platformTransactionManager.rollback(transaction1);        throw new Exception("购买商品:" + purchaseProductId + "不存在");    }        //商品当前库存    Integer currentCount = product.getCount();    //校验库存    if (purchaseProductNum > currentCount) {        platformTransactionManager.rollback(transaction1);        throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");    }​    productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());​    Order order = new Order();    // ... 省略 Set    orderMapper.insertSelective(order);​    OrderItem orderItem = new OrderItem();    orderItem.setOrderId(order.getId());    // ... 省略 Set    return order.getId();    platformTransactionManager.commit(transaction1);}

复制代码

正确示例:使用 synchronized 的代码块

public Long createOrder() throws Exception {    Product product = null;    //synchronized (this) {    //synchronized (object) {    synchronized (DBOrderService2.class) {        TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);        product = productMapper.selectByPrimaryKey(purchaseProductId);        if (product == null) {            platformTransactionManager.rollback(transaction1);            throw new Exception("购买商品:" + purchaseProductId + "不存在");        }​        //商品当前库存        Integer currentCount = product.getCount();        System.out.println(Thread.currentThread().getName() + "库存数:" + currentCount);        //校验库存        if (purchaseProductNum > currentCount) {            platformTransactionManager.rollback(transaction1);            throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");        }​        productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());        platformTransactionManager.commit(transaction1);    }​    TransactionStatus transaction2 = platformTransactionManager.getTransaction(transactionDefinition);​    Order order = new Order();    // ... 省略 Set    orderMapper.insertSelective(order);​    OrderItem orderItem = new OrderItem();    // ... 省略 Set    orderItemMapper.insertSelective(orderItem);    platformTransactionManager.commit(transaction2);    return order.getId();

复制代码

正确示例:使用 Lock

private Lock lock = new ReentrantLock();​public Long createOrder() throws Exception{      Product product = null;​    lock.lock();​    TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);    try {        product = productMapper.selectByPrimaryKey(purchaseProductId);        if (product==null){            throw new Exception("购买商品:"+purchaseProductId+"不存在");        }​        //商品当前库存        Integer currentCount = product.getCount();        System.out.println(Thread.currentThread().getName()+"库存数:"+currentCount);        //校验库存        if (purchaseProductNum > currentCount){            throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");        }​        productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());        platformTransactionManager.commit(transaction1);    } catch (Exception e) {        platformTransactionManager.rollback(transaction1);    } finally {        // 注意抛异常的时候锁释放不掉,分布式锁也一样,都要在这里删掉        lock.unlock();    }​    TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);    Order order = new Order();    // ... 省略 Set    orderMapper.insertSelective(order);​    OrderItem orderItem = new OrderItem();    // ... 省略 Set    orderItemMapper.insertSelective(orderItem);    platformTransactionManager.commit(transaction);    return order.getId();}

复制代码

三、常见分布式锁的使用

上面使用的方法只能解决单体项目,当部署多台机器的时候就会失效,因为锁本身就是单机的锁,所以需要使用分布式锁来实现。

3.1 数据库乐观锁

数据库中的乐观锁,加一个 version 字段,利用 CAS 来实现,乐观锁的方式支持多台机器并发安全。但是并发量大的时候会导致大量的 update 失败

3.2 数据库分布式锁

db 操作性能较差,并且有锁表的风险,一般不考虑。

3.2.1 简单的数据库锁

select for update

直接在数据库新建一张表:

锁的 code 预先写到数据库中,抢锁的时候,使用 select for update 查询锁对应的 key,也就是这里的 code,阻塞就说明别人在使用锁。

// 加上事务就是为了 for update 的锁可以一直生效到事务执行结束// 默认回滚的是 RunTimeException@Transactional(rollbackFor = Exception.class)public String singleLock() throws Exception {    log.info("我进入了方法!");    DistributeLock distributeLock = distributeLockMapper.        selectDistributeLock("demo");    if (distributeLock==null) {        throw new Exception("分布式锁找不到");    }    log.info("我进入了锁!");    try {        Thread.sleep(1000);    } catch (InterruptedException e) {        e.printStackTrace();    }    return "我已经执行完成!";}

复制代码

<select id="selectDistributeLock" resultType="com.deltaqin.distribute.model.DistributeLock">  select * from distribute_lock  where businessCode = #{businessCode,jdbcType=VARCHAR}  for update</select>

复制代码

使用唯一键作为限制,插入一条数据,其他待执行的 SQL 就会失败,当数据删除之后再去获取锁 ,这是利用了唯一索引的排他性。

insert lock

直接维护一张锁表:

@Autowiredprivate MethodlockMapper methodlockMapper;​@Overridepublic boolean tryLock() {    try {        //插入一条数据   insert into        methodlockMapper.insert(new Methodlock("lock"));    }catch (Exception e){        //插入失败        return false;    }    return true;}​@Overridepublic void waitLock() {    try {        Thread.sleep(10);    } catch (InterruptedException e) {        e.printStackTrace();    }}​@Overridepublic void unlock() {    //删除数据   delete    methodlockMapper.deleteByMethodlock("lock");    System.out.println("-------释放锁------");}

复制代码

3.3 Redis setNx

Redis 原生支持的,保证只有一个会话可以设置成功,因为 Redis 自己就是单线程串行执行的。

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-data-redis</artifactId></dependency>

复制代码

spring.redis.host=localhost

复制代码

封装一个锁对象:

@Slf4jpublic class RedisLock implements AutoCloseable {​    private RedisTemplate redisTemplate;    private String key;    private String value;    //单位:秒    private int expireTime;​    /**     * 没有传递 value,因为直接使用的是随机值     */    public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){        this.redisTemplate = redisTemplate;        this.key = key;        this.expireTime=expireTime;        this.value = UUID.randomUUID().toString();    }​    /**     * JDK 1.7 之后的自动关闭的功能     */    @Override    public void close() throws Exception {        unLock();    }​    /**     * 获取分布式锁     * SET resource_name my_random_value NX PX 30000     * 每一个线程对应的随机值 my_random_value 不一样,用于释放锁的时候校验     * NX 表示 key 不存在的时候成功,key 存在的时候设置不成功,Redis 自己是单线程,串行执行的,第一个执行的才可以设置成功     * PX 表示过期时间,没有设置的话,忘记删除,就会永远不过期     */    public boolean getLock(){        RedisCallback<Boolean> redisCallback = connection -> {            //设置NX            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();            //设置过期时间            Expiration expiration = Expiration.seconds(expireTime);            //序列化key            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);            //序列化value            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);            //执行setnx操作            Boolean result = connection.set(redisKey, redisValue, expiration, setOption);            return result;        };​        //获取分布式锁        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);        return lock;    }​    /**     * 释放锁的时候随机数相同的时候才可以释放,避免释放了别人设置的锁(自己的已经过期了所以别人才可以设置成功)     * 释放的时候采用 LUA 脚本,因为 delete 没有原生支持删除的时候校验值,证明是当前线程设置进去的值     * 脚本是在官方文档里面有的     */    public boolean unLock() {        // key 是自己才可以释放,不是就不能释放别人的锁        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +                "    return redis.call(\"del\",KEYS[1])\n" +                "else\n" +                "    return 0\n" +                "end";        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);        List<String> keys = Arrays.asList(key);​        // 执行脚本的时候传递的 value 就是对应的值        Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);        log.info("释放锁的结果:"+result);        return result;    }}

复制代码

每次获取的时候,自己线程需要 new 对应的 RedisLock:

public String redisLock(){    log.info("我进入了方法!");    try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){        if (redisLock.getLock()) {            log.info("我进入了锁!!");            Thread.sleep(15000);        }    } catch (InterruptedException e) {        e.printStackTrace();    } catch (Exception e) {        e.printStackTrace();    }    log.info("方法执行完成");    return "方法执行完成";}

复制代码

3.4 zookeeper 瞬时 znode 节点 + watcher 监听机制

临时节点具备数据自动删除的功能。当 client 与 ZooKeeper 连接和 session 断掉时,相应的临时节点就会被删除。zk 有瞬时和持久节点,瞬时节点不可以有子节点。会话结束之后瞬时节点就会消失,基于 zk 的瞬时有序节点实现分布式锁:

  • 多线程并发创建瞬时节点的时候,得到有序的序列,序号最小的线程可以获得锁;

  • 其他的线程监听自己序号的前一个序号。前一个线程执行结束之后删除自己序号的节点;

  • 下一个序号的线程得到通知,继续执行;

  • 以此类推,创建节点的时候,就确认了线程执行的顺序。

<dependency>  <groupId>org.apache.zookeeper</groupId>  <artifactId>zookeeper</artifactId>  <version>3.4.14</version>  <exclusions>    <exclusion>      <groupId>org.slf4j</groupId>      <artifactId>slf4j-log4j12</artifactId>    </exclusion>  </exclusions></dependency>

复制代码

zk 的观察器只可以监控一次,数据发生变化之后可以发送给客户端,之后需要再次设置监控。exists、create、getChildren三个方法都可以添加 watcher ,也就是在调用方法的时候传递 true 就是添加监听。注意这里 Lock 实现了 Watcher 和 AutoCloseable:

当前线程创建的节点是第一个节点就获得锁,否则就监听自己的前一个节点的事件:

/** * 自己本身就是一个 watcher,可以得到通知 * AutoCloseable 实现自动关闭,资源不使用的时候 */@Slf4jpublic class ZkLock implements AutoCloseable, Watcher {​    private ZooKeeper zooKeeper;​    /**     * 记录当前锁的名字     */    private String znode;​    public ZkLock() throws IOException {        this.zooKeeper = new ZooKeeper("localhost:2181",                10000,this);    }​    public boolean getLock(String businessCode) {        try {            //创建业务 根节点            Stat stat = zooKeeper.exists("/" + businessCode, false);            if (stat==null){                zooKeeper.create("/" + businessCode,businessCode.getBytes(),                        ZooDefs.Ids.OPEN_ACL_UNSAFE,                        CreateMode.PERSISTENT);            }​            //创建瞬时有序节点  /order/order_00000001            znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),                    ZooDefs.Ids.OPEN_ACL_UNSAFE,                    CreateMode.EPHEMERAL_SEQUENTIAL);​            //获取业务节点下 所有的子节点            List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);            //获取序号最小的(第一个)子节点            Collections.sort(childrenNodes);            String firstNode = childrenNodes.get(0);            //如果创建的节点是第一个子节点,则获得锁            if (znode.endsWith(firstNode)){                return true;            }            //如果不是第一个子节点,则监听前一个节点            String lastNode = firstNode;            for (String node:childrenNodes){                if (znode.endsWith(node)){                    zooKeeper.exists("/"+businessCode+"/"+lastNode,true);                    break;                }else {                    lastNode = node;                }            }            synchronized (this){                wait();            }            return true;        } catch (Exception e) {            e.printStackTrace();        }        return false;    }​    @Override    public void close() throws Exception {        zooKeeper.delete(znode,-1);        zooKeeper.close();        log.info("我已经释放了锁!");    }​    @Override    public void process(WatchedEvent event) {        if (event.getType() == Event.EventType.NodeDeleted){            synchronized (this){                notify();            }        }    }}

复制代码

3.5 zookeeper curator

在实际的开发中,不建议去自己“重复造轮子”,而建议直接使用 Curator 客户端中的各种官方实现的分布式锁,例如其中的 InterProcessMutex 可重入锁。

<dependency>  <groupId>org.apache.curator</groupId>  <artifactId>curator-recipes</artifactId>  <version>4.2.0</version>  <exclusions>    <exclusion>      <artifactId>slf4j-api</artifactId>      <groupId>org.slf4j</groupId>    </exclusion>  </exclusions></dependency>

复制代码

@Bean(initMethod="start",destroyMethod = "close")public CuratorFramework getCuratorFramework() {    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);    CuratorFramework client = CuratorFrameworkFactory.        newClient("localhost:2181", retryPolicy);    return client;}

复制代码

框架已经实现了分布式锁。zk 的 Java 客户端升级版。使用的时候直接指定重试的策略就可以。

官网中分布式锁的实现是在curator-recipes依赖中,不要引用错了。

@Autowiredprivate CuratorFramework client;​@Testpublic void testCuratorLock(){    InterProcessMutex lock = new InterProcessMutex(client, "/order");    try {        if ( lock.acquire(30, TimeUnit.SECONDS) ) {            try  {                log.info("我获得了锁!!!");            }            finally  {                lock.release();            }        }    } catch (Exception e) {        e.printStackTrace();    }    client.close();}

复制代码

3.6 Redission

重新实现了 Java 并发包下处理并发的类,让其可以跨 JVM 使用,例如 CHM 等。

3.6.1 非 SpringBoot 项目引入

https://redisson.org/

引入 Redisson 的依赖,然后配置对应的 XML 即可:

<dependency>  <groupId>org.redisson</groupId>  <artifactId>redisson</artifactId>  <version>3.11.2</version>  <exclusions>    <exclusion>      <artifactId>slf4j-api</artifactId>      <groupId>org.slf4j</groupId>    </exclusion>  </exclusions></dependency>

复制代码

编写相应的 redisson.xml

<beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xmlns:redisson="http://redisson.org/schema/redisson"       xsi:schemaLocation="       http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.springframework.org/schema/context       http://www.springframework.org/schema/context/spring-context.xsd       http://redisson.org/schema/redisson       http://redisson.org/schema/redisson/redisson.xsd">​    <redisson:client>        <redisson:single-server address="redis://127.0.0.1:6379"/>    </redisson:client></beans>

复制代码

配置对应@ImportResource("classpath*:redisson.xml")资源文件。

3.6.2 SpringBoot 项目引入

或者直接使用 springBoot 的 starter 即可。

https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter

<dependency>  <groupId>org.redisson</groupId>  <artifactId>redisson-spring-boot-starter</artifactId>  <version>3.19.1</version></dependency>

复制代码

修改 application.properties 即可:#spring.redis.host=

3.6.3 设置配置类

@Beanpublic RedissonClient getRedissonClient() {    Config config = new Config();    config.useSingleServer().setAddress("redis://127.0.0.1:6379");    return Redisson.create(config);}

复制代码

3.6.4 使用

@Testpublic void testRedissonLock() {    RLock rLock = redisson.getLock("order");    try {        rLock.lock(30, TimeUnit.SECONDS);        log.info("我获得了锁!!!");        Thread.sleep(10000);    } catch (InterruptedException e) {        e.printStackTrace();    }finally {        log.info("我释放了锁!!");        rLock.unlock();    }}

复制代码

3.7 Etcd

参考以下文章,普通项目不会为了一把锁引入 etcd,此处不再赘述:

21 | 分布式锁:为什么基于etcd实现分布式锁比Redis锁更安全?-极客时间

四、常见分布式锁的原理

4.1 Redisson

Redis 2.6 之后才可以执行 lua 脚本,比起管道而言,这是原子性的,模拟一个商品减库存的原子操作:

//lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10jedis.set("product_stock_10016", "15");  //初始化商品10016的库存String script = " local count = redis.call('get', KEYS[1]) " +        " local a = tonumber(count) " +        " local b = tonumber(ARGV[1]) " +        " if a >= b then " +        "   redis.call('set', KEYS[1], a-b) " +        "   return 1 " +        " end " +        " return 0 ";Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"),                         Arrays.asList("10"));System.out.println(obj);

复制代码

4.1.1 尝试加锁的逻辑

上面的org.redisson.RedissonLock#lock()通过调用自己方法内部的 lock 方法的org.redisson.RedissonLock#tryAcquire方法。之后调用 org.redisson.RedissonLock#tryAcquireAsync:

首先调用内部的org.redisson.RedissonLock#tryLockInnerAsync:设置对应的分布式锁

到这里获取锁的逻辑就结束了,如果这里没有获取到,在 Future 的回调里面就会直接 return,会在外层有一个 while true 的循环,订阅释放锁的消息准备被唤醒。如果说加锁成功,就开始执行锁续命逻辑。

4.1.2 锁续命逻辑

lua 脚本最后是以毫秒为单位返回 key 的剩余过期时间。成功加锁之后org.redisson.RedissonLock#scheduleExpirationRenewal中将会调用org.redisson.RedissonLock#renewExpiration,这个方法内部就有锁续命的逻辑,是一个定时任务,等 10s 执行。

执行的时候尝试执行的续命逻辑使用的是 Lua 脚本,当前的锁有值,就续命,没有就直接返回 0:

返回 0 之后外层会判断,延时成功就会再次调用自己,否则延时调用结束,不再为当前的锁续命。所以这里的续命不是一个真正的定时,而是循环调用自己的延时任务。

4.1.3 循环间隔抢锁机制

如果一开始就加锁成功就直接返回。

如果一开始加锁失败,没抢到锁的线程就会在 while 循环中尝试加锁,加锁成功就结束循环,否则等待当前锁的超时时间之后再次尝试加锁。所以实现逻辑默认是非公平锁:

里面有一个 subscribe 的逻辑,会监听对应加锁的 key,当锁释放之后 publish 对应的消息,此时如果没有到达对应的锁的超时时间,也会尝试获取锁,避免时间浪费。

4.1.4 释放锁和唤醒其他线程的逻辑

前面没有抢到锁的线程会监听对应的 queue,后面抢到锁的线程释放锁的时候会发送一个消息。

订阅的时候指定收到消息时候的逻辑:会唤醒阻塞之后执行 while 循环

4.1.5 重入锁的逻辑

存在对应的锁,就对对应的 hash 结构的 value 直接+1,和 Java 重入锁的逻辑是一致的。

4.2 RedLock 解决非单体项目的 Redis 主从架构的锁失效

https://redis.io/docs/manual/patterns/distributed-locks/

查看 Redis 官方文档,对于单节点的 Redis ,使用 setnx 和 lua del 删除分布式锁是足够的,但是主从架构的场景下:锁先加在一个 master 节点上,默认是异步同步到从节点,此时 master 挂了会选择 slave 为 master,此时又可以加锁,就会导致超卖。但是如果使用 zookeeper 来实现的话,由于 zk 是 CP 的,所以 CP 不存在这样的问题。

Redis 文档中给出了 RedLock 的解决办法,使用 redLock 真的可以解决吗?

4.2.1 RedLock 原理

基于客户端的实现,是基于多个独立的 Redis Master 节点的一种实现(一般为 5)。client 依次向各个节点申请锁,若能从多数个节点中申请锁成功并满足一些条件限制,那么 client 就能获取锁成功。它通过独立的 N 个 Master 节点,避免了使用主备异步复制协议的缺陷,只要多数 Redis 节点正常就能正常工作,显著提升了分布式锁的安全性、可用性。

注意图中所有的节点都是 master 节点。加锁超过半数成功,就认为是成功。具体流程:

  • 获取锁

    获取当前时间 T1,作为后续的计时依据;

    按顺序地,依次向 5 个独立的节点来尝试获取锁 SET resource_name my_random_value NX PX 30000;

    计算获取锁总共花了多少时间,判断获取锁成功与否;

    时间:T2-T1;

    多数节点的锁(N/2+1);

    当获取锁成功后的有效时间,要从初始的时间减去第三步算出来的消耗时间;

    如果没能获取锁成功,尽快释放掉锁。

  • 释放锁

    向所有节点发起释放锁的操作,不管这些节点有没有成功设置过。

public String redlock() {    String lockKey = "product_001";    //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了    RLock lock1 = redisson.getLock(lockKey);    RLock lock2 = redisson.getLock(lockKey);    RLock lock3 = redisson.getLock(lockKey);​    /**     * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)     */    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);    try {        /**         * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败         * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)         */        boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);        if (res) {            //成功获得锁,在这里处理业务        }    } catch (Exception e) {        throw new RuntimeException("lock fail");    } finally {        //无论如何, 最后都要解锁        redLock.unlock();    }​    return "end";}

复制代码

但是,它的实现建立在一个不安全的系统模型上的,它依赖系统时间,当时钟发生跳跃时,也可能会出现安全性问题。分布式存储专家 Martin 对 RedLock 的分析文章,Redis 作者的也专门写了一篇文章进行了反驳。

Martin Kleppmann:How to do distributed locking

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Antirez:Is Redlock safe?

http://antirez.com/news/101

4.2.2 RedLock 问题一:持久化机制导致重复加锁

如果是上面的架构图,一般生产都不会配置 AOF 的每一条命令都落磁盘,一般会设置一些间隔时间,比如 1s,如果 ABC 节点加锁成功,有一个节点 C 恰好是在 1s 内加锁,还没有落盘,此时挂了,就会导致其他客户端通过 CDE 又会加锁成功。

4.2.3 RedLock 问题二:主从下重复加锁

除非多部署一些节点,但是这样会导致加锁时间变长,这样比较下来效果就不如 zk 了。

4.2.4 RedLock 问题三:时钟跳跃导致重复加锁

C 节点发生了时钟跳跃,导致加上的锁没有到达实际的超时时间,就被误以为超时而释放,此时其他客户端就可以重复加锁了。

4.3 Curator

InterProcessMutex 可重入锁的分析

五、业务中使用分布式锁的注意点

获取的锁要设置有效期,假设我们未设置 key 自动过期时间,在 Set key value NX 后,如果程序 crash 或者发生网络分区后无法与 Redis 节点通信,毫无疑问其他 client 将永远无法获得锁,这将导致死锁,服务出现中断。

SETNX 和 EXPIRE 命令去设置 key 和过期时间,这也是不正确的,因为你无法保证 SETNX 和 EXPIRE 命令的原子性。

自己使用 setnx 实现 Redis 锁的时候,注意并发情况下不要释放掉别人的锁(业务逻辑执行时间超过锁的过期时间),导致恶性循环。一般:

1)加锁的时候需要指定 value 的内容是当前进程中的当前线程的唯一标记,不要使用线程 ID 作为当前线程的锁的标记,因为不同实例上的线程 ID 可能是一样的。

2)释放锁的逻辑会写在 finally ,释放锁时候要判断锁对应的 value,而且要使用 lua 脚本实现原子 del 操作。因为 if 逻辑判断完之后也可能失效导致删除别人的锁。

3)针对扣减库存这个逻辑,lua 脚本里面实现 Redis 比较库存、扣减库存操作的原子性。通过判断 Redis Decr 命令的返回值即可。此命令会返回扣减后的最新库存,若小于 0 则表示超卖。

5.1 自己实现分布式锁的坑

5.1.1 setnx 不关心锁的顺序导致删除别人的锁

锁失效之后,别人加锁成功,自己把别人的锁删了。

我们无法预估程序执行需要的锁的时间。

public String deductStock() {    String lockKey = "lock:product_101";    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "deltaqin");    stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);​    try {        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")        if (stock > 0) {            int realStock = stock - 1;            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)            System.out.println("扣减成功,剩余库存:" + realStock);        } else {            System.out.println("扣减失败,库存不足");        }    } finally {        stringRedisTemplate.delete(lockKey);    }​    return "end";}

复制代码

5.1.2 setnx 关心锁的顺序还是删除了别人的锁

并发会卡在各种地方,卡住的时候过期了,就会删掉别人加的锁:

错误的原因还是因为解锁的逻辑不是原子性的,这里可以参考 Redisson 的解锁逻辑使用 lua 脚本实现。

public String deductStock() {    String lockKey = "lock:product_101";    String clientId = UUID.randomUUID().toString();    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)    if (!result) {        return "error_code";    }    try {        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")        if (stock > 0) {            int realStock = stock - 1;            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)            System.out.println("扣减成功,剩余库存:" + realStock);        } else {            System.out.println("扣减失败,库存不足");        }    } finally {        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {            // 卡在这里,锁过期了,其他线程又可以加锁,此时又把其他线程新加的锁删掉了            stringRedisTemplate.delete(lockKey);        }    }    return "end";}

复制代码

解决办法

这种问题解决的办法就是使用锁续命,比如使用一个定时任务间隔小于锁的超时时间,每隔一段时间就给锁续命,除非线程自己主动删除。这也是 Redisson 的实现思路。

5.2 锁优化:分段加锁逻辑

针对一个商品,要开启秒杀的时候,会将商品的库存预先加载到 Redis 缓存中,比如有 100 个库存,此时可以分为 5 个 key,每一个 key 有 20 个库存。可以把分布式锁的性能提升 5 倍。

例如:

  • product_10111_stock = 100

    product_10111_stock1 = 20

    product_10111_stock2 = 20

    product_10111_stock3 = 20

    product_10111_stock4 = 20

    product_10111_stock5 = 20

请求来了可以随机可以轮询,扣减完之后就标记不要下次再分配到这个库存。

六、分布式锁的真相与选择

6.1 分布式锁的真相

需要满足的几个特性

  • 互斥:不同线程、进程互斥。

  • 超时机制:临界区代码耗时导致,网络原因导致。可以使用额外的线程续命保证。

  • 完备的锁接口:阻塞的和非阻塞的接口都要有,lock 和 tryLock。

  • 可重入性:当前请求的节点+ 线程唯一标识。

  • 公平性:锁唤醒时候,按照顺序唤醒。

  • 正确性:进程内的锁不会因为报错死锁,因为崩溃的时候整个进程都会结束。但是多实例部署时死锁就很容易发生,如果粗暴使用超时机制解决死锁问题,就默认了下面这个假设:

    锁的超时时间 >> 获取锁的时延 + 执行临界区代码的时间 + 各种进程的暂停(比如 GC)

    但上述假设其实无法保证的。

将分布式锁定位为,可以容忍非常小概率互斥语义失效场景下的锁服务。一般来说,一个分布式锁服务,它的正确性要求越高,性能可能就会越低。

6.2 分布式锁的选择

  • 数据库:db 操作性能较差,并且有锁表的风险,一般不考虑。

    优点:实现简单、易于理解

    缺点:对数据库压力大

  • Redis:适用于并发量很大、性能要求很高而可靠性问题可以通过其他方案去弥补的场景。

    优点:易于理解

    缺点:自己实现、不支持阻塞

    Redisson:相对于 Jedis 其实更多用在分布式的场景。

    优点:提供锁的方法,可阻塞

  • Zookeeper:适用于高可靠(高可用),而并发量不是太高的场景。

    优点:支持阻塞

    缺点:需理解 Zookeeper、程序复杂

  • Curator

    优点:提供锁的方法

    缺点:Zookeeper,强一致,慢

  • Etcd:安全和可靠性上有保证,但是比较重。

不推荐自己编写的分布式锁,推荐使用 Redisson 和 Curator 实现的分布式锁。

RK3568平台开发系列讲解:UBOOT详细介绍 U-boot是一款广泛使用的开源引导加载程序,支持多种嵌入式操作系统和处理器架构。其主要功能包括系统上电时的硬件初始化、引导操作系统内核等。U-boot的引导流程通常包括Boot Code、idbloader.img、uboot.img、boot.img和rootfs.img等步骤。启动U-boot时,通过Debug串口连接板卡,按下ctrl+c键可进入U-Boot命令行模式。U-boot启动过程中会执行一系列初始化操作,如打印系统信息、初始化内存、加载内核设备树、初始化I2C、PMIC电源芯片、DRM框架 阅读详情

相关推荐

Python爬虫实战:5分钟搞定东方财富网股票数据抓取(附完整代码)

本文详细介绍了如何利用Python爬虫技术,快速抓取东方财富网的实时股票数据。通过逆向分析其JSONP数据接口,构建了包含请求处理、数据清洗、分页调度及多格式存储的完整自动化采集系统,并提供了应对反爬策略与生产部署的进阶优化思路,附有可直接运行的完整代码。

weixin_29084231的博客 874

比较 Redisson 和 Curator:分布式锁实现

Redisson 是一个基于 Redis 的分布式 Java 对象和服务库,它不仅提供了分布式锁,还支持多种其他分布式数据结构,如分布式集合、队列、Map 等。它提供了丰富的功能,适用于各种分布式系统场景。Redisson是基于Redis的,它使用Redis作为后端存储,因此依赖于Redis的可用性和性能。Redisson不仅提供分布式锁,还支持多种其他分布式数据结构,如分布式集合、队列、Map等。

~ 1234

ArcMap新手必看:5分钟搞定面要素的四至信息提取(附字段重命名技巧)

本文为ArcMap新手提供了快速提取面要素四至信息的详细教程,包括数据准备、坐标系选择、批量处理技巧及字段重命名优化方案。通过实战步骤和Python脚本示例,帮助用户高效完成GIS数据处理任务,特别适合行政区划数据分析和报告生成。

weixin_29266007的博客 384

Redis分布式锁:原理、实现与最佳实践

Redis分布式锁是解决分布式系统并发控制的有效工具,通过合理使用Redis的原子操作和过期机制,可以实现高效、可靠的分布式锁。在实际应用中,应根据系统的需求和特点,选择合适的实现方式和最佳实践,确保系统的正确性和性能。对于要求极高可靠性的场景,可以考虑使用Redlock算法或其他分布式协调服务如ZooKeeper、etcd等。对于一般场景,使用Redisson等成熟的客户端库已经能够满足大多数需求。

exlink2012的专栏 1005

哈工大OS实验四——进程运行轨迹的跟踪与统计

进程运行轨迹的跟踪与统计 进程从创建(Linux 下调用 fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出 CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行…… process.c解读 void cpuio_bound(int last, int cpu_

优快云-ych 1001

Open3D 点云数学形态学滤波(Python版本)

Open3D 点云数学形态学滤波(Python版本)

dayuhaitang1的博客 1396

Redis分布式锁详解:原理、实现与最佳实践

在分布式系统中,经常需要对共享资源进行同步访问,以避免并发问题。Redis分布式锁提供了一种简单而高效的方式来实现这一需求。本文将深入探讨Redis分布式锁的工作原理、实现方法以及在实际应用中的一些最佳实践。

2301_77695569的博客 1811

Dify 第 2 篇 Dify 插件离线安装

Dify是一个支持插件扩展的AI平台,其插件市场提供5种类型插件(模型、工具、Agent策略、扩展、插件包)。由于默认pip安装速度慢,可通过dify-plugin-repackaging工具将插件转换为离线包(使用阿里云镜像加速)。该工具提供两种方式:1)自备Python环境执行脚本;2)使用Docker环境(推荐)。脚本支持从Dify市场或GitHub下载插件,并重新打包为含依赖的离线包,便于通过"本地插件"方式快速安装。

甘蓝的专栏 1261

深入解析分布式锁的实现原理与最佳实践

分布式锁是解决分布式系统资源竞争的有效手段,但并非银弹。根据业务特点选择合适实现方案理解所选方案的局限性和边界条件设计完善的监控和降级方案有时候,避免使用分布式锁可能是更好的选择。使用CAS操作(如库存扣减)采用消息队列串行化处理使用乐观锁控制并发如果你对分布式系统感兴趣,可以尝试从0打造个人豆包实时通话AI动手实验,体验如何构建一个完整的分布式应用。我在实际操作中发现,通过这个实验可以很好地理解分布式系统各组件如何协同工作。基于火山引擎豆包大模型,从零搭建一个实时语音通话应用。

2600_94959855的博客 764

cesium实战目录必看

本文作者长期从事GIS相关工作,专注于三维和地图可视化领域,熟悉Cesium、Three.js、WebGL等开发框架。文章目录展示了作者在Cesium应用中的多项技术实践,涵盖水面反射、离屏渲染、后处理效果、地形裁剪、Shader着色器、动态单体化、气象图、缓冲区域、坡度和坡向分析、三维热力图、通视分析、态势标绘等多个方面。这些内容为开发者提供了丰富的技术参考,展示了Cesium在三维可视化中的强大功能和应用场景。

God's asylum的博客 8718

prometheus普罗米修斯集监控外部k8s

prometheus普罗米修斯集监控外部k8s

你是我一生的追求 2247

Redis 分布式锁:原理、实现及最佳实践

在单机系统中,使用传统的互斥锁机制(如操作系统的锁、数据库锁)来控制对共享资源的访问是相对容易实现的。然而,在分布式系统中,由于有多个节点、多个进程以及不同物理机器之间的网络通信,这种集中化的锁机制已经无法简单地适用。分布式锁的出现正是为了在多个客户端之间对共享资源的访问进行协调,确保数据的一致性和完整性。

lssffy的博客 3619

STM32 cubeMX配置串口重定向

这篇文章主要讲解什么是串口重定向,为什么要串口串口重定向。使用printf函数和上位机通信是非常的方便的。希望大家都能够掌握。

m0_49476241的博客 4572

Redis 实现分布式锁:原理、实践与最佳实践

必须使用唯一标识:确保释放锁时不会误删其他客户端的锁设置合理的过期时间:结合业务场景评估,避免过短或过长避免锁粒度过大:减小锁的范围,提高并发效率使用集群部署:通过主从+哨兵或 Redis Cluster 保证高可用失败重试机制:获取锁失败时,采用有限次数的重试策略监控与告警:对锁的获取成功率、持有时间等指标进行监控基于 Redis 的分布式锁实现需在性能、安全性和可用性之间寻找平衡。理解其核心原理并结合业务场景选择合适的方案,才能构建可靠的分布式锁服务。

艾斯比 798

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

架构思维:分布式锁设计与实现_原理、方案与最佳实践

分布式锁的选择需要综合业务场景、性能需求与运维能力。理解不同方案的实现原理和限制条件,才能设计出既保证数据一致性,又具备高可用的分布式系统。

小工匠 2729

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

分布式锁深度解析:原理、实现与最佳实践

根据场景选择方案高并发、允许极小概率锁失效 → Redis(配合 Redisson)强一致性、可靠性要求极高 → ZooKeeper 或 etcd已有基础设施、低并发 → 数据库使用成熟客户端:避免重复造轮子,Redisson、Curator、Jetcd 已经处理了续期、重入、原子性等细节。设置合理的超时时间:结合业务最大耗时 + 缓冲时间,并启用续期。确保解锁的幂等性和安全性:解锁前必须校验持有者。监控与告警:监控锁服务健康状况、锁等待时间、死锁频率。业务层面配合。

itsniu的博客 709

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Redis分布式锁实现原理与最佳实践

分布式锁是解决分布式系统资源竞争的关键技术,其核心在于实现跨进程的互斥访问。基于Redis的分布式锁利用SETNX命令和过期时间机制,通过原子操作确保锁的互斥性和自动释放。在实际工程中,Redis锁的高性能特性使其成为秒杀系统、订单处理等高并发场景的首选方案。针对锁误删、时钟漂移等典型问题,可通过唯一标识验证和NTP时间同步等方案解决。对于金融级系统,Redlock算法结合多节点部署能显著提升可靠性。合理设置锁粒度和过期时间,配合Redisson等工具链,可以构建出既满足性能要求又保证数据一致性的分布式锁体

weixin_32466193的博客 248

Java中的分布式锁:原理、实现与最佳实践

分布式锁是构建高可靠分布式系统的基石,但过度依赖锁可能降低系统吞吐量。在实际开发中,应结合业务场景选择最简方案,优先考虑无锁设计(如CAS操作、本地队列)。通过理解不同实现背后的权衡,开发者能够在一致性、性能和复杂度之间找到最佳平衡点。

qq_56438516的博客 1820

分布式环境下高可靠性分布式锁实现指南:原理、实战与最佳实践

本文深入探讨分布式环境下高可靠性分布式锁的实现方案。首先分析分布式锁的核心特性(互斥性、安全性、可用性等)及设计原则,然后对比三种主流实现方案:基于Redis的高性能方案(推荐Redisson框架)、基于ZooKeeper的高可靠方案(推荐Curator框架)以及基于数据库的轻量方案。针对每种方案详细说明其原理、适用场景和实战代码实现,并提供锁粒度设计、过期时间设置、集群部署等最佳实践建议。文章强调应根据业务场景选择合适的方案,在高并发场景优先选择Redis,高可靠场景优先选择ZooKeeper,并注意异常

what丶k的博客 807
上一篇: 京东二面,Redis 为什么这么快?
下一篇: 从数据到价值,DataOps 精益数据运营概述
wrr-cat
博客等级 码龄9年 128粉丝 302原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值