分布式锁机制

分布式锁机制

在分布式项目中我们在处理线程安全问题时,通过加锁是行不通的,因为我们程序跑在多台机器上,每台机器拿到的是不同的锁对象,因此加锁无法避免线程安全问题,因此需要引入分布式锁的解决方案

引入分布式锁案例

// 库存扣减
package com.zhj.bean;

/**
 * @author zhj
 */
public class Stock {
    // 库存
    private static int num = 1;

    // 减少库存
    public boolean reduceStock() {
        if (num > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num--;
            return true;
        } else {
            return false;
        }
    }
}

测试方法

package com.zhj.test;

import com.zhj.bean.Stock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author zhj
 */
public class stockMain {

    private static Lock lock = new ReentrantLock();

    static class StockTask implements Runnable {

        @Override
        public void run() {

            lock.lock();

            try {
                boolean b = new Stock().reduceStock();
                if (b) {
                    System.out.println(Thread.currentThread().getName() + " 减少库存成功");
                } else {
                    System.out.println(Thread.currentThread().getName() + " 减少库存失败");
                }
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(new StockTask(),"线程" + i).start();
        }
    }
}

1. 使用数据库解决分布式锁问题

创建数据表

CREATE TABLE `lock_db`.`lock_record`  (
  `id` int NOT NULL,
  `lock_name` varchar(255) NOT NULL COMMENT '唯一锁名称',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `unique_lock_name`(`lock_name`) COMMENT '唯一索引'
);

通过数据库添加一条唯一数据进行加锁操作,解锁是删除该条数据

数据库解决分布式锁

package com.zhj.lock;

import com.zhj.bean.LockRecord;
import com.zhj.mapper.LockRecordMapper;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author zhj
 */
@Component
public class DBLock implements Lock {

    private static final String LOCK_NAME = "db_lock_stock";
    @Resource
    private LockRecordMapper lockRecordMapper;

    @Override
    public void lock() {
        while (true) {
            boolean flag = tryLock();
            if (flag) {
                try {
                    LockRecord lockRecord = new LockRecord();
                    lockRecord.setLockName(LOCK_NAME);
                    lockRecordMapper.insert(lockRecord);
                    return;
                } catch (Exception e) {
                    System.out.println("-------");
                }
            } else {
                System.out.println("等待中...");
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    /**
     * 尝试获取锁,根据指定的名称在数据表发起一次查询
     * @return
     */
    @Override
    public boolean tryLock() {
        Example example = new Example(LockRecord.class);
        example.createCriteria().andEqualTo("lockName", LOCK_NAME);
        LockRecord lockRecord = lockRecordMapper.selectOneByExample(example);
        if (lockRecord == null) {
            return true;
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        Example example = new Example(LockRecord.class);
        example.createCriteria().andEqualTo("lockName", LOCK_NAME);
        lockRecordMapper.deleteByExample(example);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

2. 使用Redis解决分布式锁问题

通过设置过期时间避免死锁出现

package com.zhj.lock;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author zhj
 */
@Component
public class RedisLock implements Lock {

    private static final String LOCK_NAME = "redis_lock_stock";

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public void lock() {
        while (true) {

            // unlock 未执行会出现死锁,我们可以通过设置过期时间防止死锁
            // Boolean flag = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME);
            Boolean flag = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME,15,TimeUnit.SECONDS);
            if (flag) {
                return;
            } else {
                System.out.println("等待获取redis锁");
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        redisTemplate.delete("lockName");
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

使用官方推荐redisson实现

为解决单点问题,我们常使用redis集群,那么使用redis时可能出现主节点突然挂掉的事件,没来得及同步数据,这样就会两个客户端同时拥有,出现超卖现象。redis官方推迟Redlock算法解决分布式问题的redisson

1.导入对应依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>

2.使用

package com.zhj.test;

import com.zhj.bean.Stock;
import com.zhj.lock.RedisLock;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.annotation.Resource;

/**
 * @author zhj
 */
public class stockMain {

    // private static Lock lock = new ReentrantLock();
    // 分布式锁
    @Resource
    private static RedisLock redisLock;
    // private static DBLock dbLock;
    private static RLock rLock;

    static {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // dbLock = classPathXmlApplicationContext.getBean(DBLock.class);
        redisLock = classPathXmlApplicationContext.getBean(RedisLock.class);
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1);
        Redisson redisson = (Redisson) Redisson.create(config);
        rLock = redisson.getLock("redis_lock_stock");
    }

    static class StockTask implements Runnable {

        @Override
        public void run() {

            // dbLock.lock();
            // redisLock.lock();
            rLock.lock();

            boolean b = new Stock().reduceStock();

            // dbLock.unlock();
            // redisLock.unlock();
            rLock.unlock();
            if (b) {
                System.out.println(Thread.currentThread().getName() + " 减少库存成功");
            } else {
                System.out.println(Thread.currentThread().getName() + " 减少库存失败");
            }
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(new StockTask(),"线程" + i).start();
        }
    }
}

3. 使用zookeeper实现分布式锁

在这里插入图片描述

为每一个线程创建一个有序的临时节点,为了确保有序性,在创建完节点,会再回去全部节点,再重新进行一次排序,排序过程中,每个线程要判断自己剩下的临时节点的序号是否是最小。如果是最新,将会获得锁,执行相关操作,释放锁。如果不是最小的,会监听它前的一个节点,当前一个节点被删除时,它会获得锁,依次类推

package com.zhj.lock;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author zhj
 */
public class ZKLock implements Lock {
    // 客户端
    private ZooKeeper zk;
    // zk 是一个目录结构,locks
    private String root = "/locks";
    // 锁的名称
    private String lockName;
    // 当前线程创建的序列node
    private ThreadLocal<String> nodeId = new ThreadLocal<>();
    // 用来同步等待zkClient链接到了服务端
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    private final static int sessionTimeout = 3000;
    private final static byte[] data = new byte[0];

    public ZKLock(String config, String lockName) {
        this.lockName = lockName;

        try {
            zk = new ZooKeeper(config, sessionTimeout, new Watcher() {

                @Override
                public void process(WatchedEvent event) {
                    // 建立连接
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        connectedSignal.countDown();
                    }
                }

            });

            connectedSignal.await();
            Stat stat = zk.exists(root, false);
            if (null == stat) {
                // 创建根节点
                zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 监听临时节点的删除行为
    class LockWatcher implements Watcher {
        private CountDownLatch latch = null;
        public LockWatcher(CountDownLatch latch) {
            this.latch = latch;
        }
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeDeleted) {
                latch.countDown();
            }
        }
    }

    @Override
    public void lock() {
        try {
            // 创建临时子节点
            String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(Thread.currentThread().getName()+myNode+ "created");

            // 取出所有子节点
            List<String> subNodes = zk.getChildren(root, false);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for(String node :subNodes) {
                sortedNodes.add(root +"/" +node);
            }

            String smallNode = sortedNodes.first();

            if (myNode.equals(smallNode)) {
                // 如果是最小的节点,则表示取得锁
                System.out.println(Thread.currentThread().getName()+ myNode+"get lock");
                this.nodeId.set(myNode);
                return;
            }

            String preNode = sortedNodes.lower(myNode);

            CountDownLatch latch = new CountDownLatch(1);
            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
            // 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
            if (stat != null) {
                System.out.println(Thread.currentThread().getName()+myNode+
                        " waiting for " + root + "/" + preNode + " released lock");

                latch.await();// 等待,这里应该一直等待其他线程释放锁
                nodeId.set(myNode);
                latch = null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        try {
            System.out.println(Thread.currentThread().getName()+ "unlock ");
            if (null != nodeId) {
                zk.delete(nodeId.get(), -1);
            }
            nodeId.remove();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

分布式队列

队列特性:先进先出

在这里插入图片描述

在这里插入图片描述

<think>嗯,用户让我介绍一下Redis分布式锁机制。首先,我需要回忆一下自己对这个主题的理解。分布式锁是用于在分布式系统中协调不同节点对共享资源的访问,确保同一时间只有一个客户端可以操作资源。Redis作为一个常用的内存数据库,确实常被用来实现这种锁机制。 接下来,我应该先讲清楚分布式锁的基本概念应用场景。比如,在电商系统中,防止超卖或者重复处理订单等情况。然后,可能需要提到为什么需要分布式锁,单机锁在分布式环境下无法满足需求,对吧? 然后,Redis实现分布式锁的常见方法,最经典的可能就是使用SET命令结合NXPX选项。NX表示只有当键不存在时才设置,PX设置过期时间。这样可以避免死锁,因为即使客户端崩溃,锁也会自动释放。不过,这里可能会有问题,比如锁过期后业务还没执行完,导致其他客户端获取锁,这时候可能会有竞争条件。 接下来,Redlock算法应该要提到,这是Redis官方推荐的分布式锁算法,用于在多个独立的Redis节点上实现更健壮的锁。需要解释Redlock的基本步骤:获取当前时间,依次向多个节点申请锁,计算获取锁的时间,如果超过半数成功并且总耗时小于锁的有效时间,才算成功。否则,需要向所有节点释放锁。 不过,用户可能需要知道这些方法的优缺点。比如,基于单节点的SETNX可能不够可靠,尤其是在主从架构下,主节点宕机可能导致锁信息丢失。而Redlock虽然更可靠,但实现复杂,性能可能受影响,并且也有争议,比如Martin Kleppmann曾指出其潜在问题,需要权衡使用。 此外,还需要提到一些最佳实践,比如设置合理的过期时间,避免长时间占用锁;使用唯一标识(如UUID)作为锁的值,确保只有锁的持有者才能释放,防止误删。释放锁时最好使用Lua脚本保证原子性,因为判断删除两个操作需要原子执行,否则可能删除其他客户端的锁。 用户可能还会关心实际应用中的问题,比如锁的重入、锁的续期(看门狗机制),或者如何避免锁被其他进程释放。这时候可能需要提到Redisson这样的客户端库,它们封装了这些复杂逻辑,提供了更易用的接口。 不过,用户的问题可能比较基础,所以需要保持回答的结构清晰,分点讲解,从基本实现到高级算法,再到注意事项,逐步深入。同时,要确保LaTeX格式正确,数学表达式用$...$包裹,独立公式用$$...$$,并且用中文回答,避免技术术语使用错误。 需要检查是否有遗漏的关键点,比如锁的释放机制、原子操作的重要性、Redlock的详细步骤、以及可能的失败场景处理方法。此外,要强调分布式锁并不能解决所有并发问题,需要结合业务场景谨慎设计。 最后,总结一下Redis分布式锁的优缺点,帮助用户做出合适的选择。可能用户在实际应用中会遇到具体问题,但至少通过这个回答,他们应该能掌握基本原理实现方式,以及需要注意的陷阱解决方案。</think>### Redis分布式锁机制详解 --- #### 一、**基本概念** **分布式锁**是分布式系统中用于协调多个进程/服务**互斥访问共享资源**的机制Redis因其高性能原子性操作,常被用于实现分布式锁。 --- #### 二、**核心实现原理** Redis分布式锁的核心是**通过原子性操作占据唯一标识**,典型实现步骤如下: 1. **加锁** - 使用`SET`命令结合`NX`(不存在才设置)`PX`(过期时间)参数: ```redis SET lock_key unique_value NX PX 30000 ``` - `lock_key`:锁的唯一标识 - `unique_value`:客户端生成的唯一值(如UUID),用于安全释放锁 - `PX 30000`:锁自动过期时间(单位毫秒),避免死锁 2. **释放锁** - 通过Lua脚本保证**原子性判断与删除**: ```lua if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end ``` - 先验证`unique_value`是否匹配,再删除锁,防止误删其他客户端的锁。 --- #### 三、**关键问题与解决方案** 1. **锁过期但业务未完成** - **问题**:锁自动释放后,业务逻辑仍在执行,导致多客户端同时操作资源。 - **解决方案**: - 设置合理的过期时间(略大于业务执行时间)。 - 使用**看门狗机制**(如Redisson库):后台线程定期续期锁。 2. **主从架构下的锁失效** - **问题**:主节点写入锁后宕机,从节点未同步锁信息,导致锁丢失。 - **解决方案**: - 使用**RedLock算法**(需部署多个独立Redis实例): $$ T_{\text{total}} = T_1 + T_2 + \cdots + T_N \leq T_{\text{expire}} $$ - 客户端依次向N个实例申请锁,当半数以上($\lfloor N/2 \rfloor +1$)成功时,认为加锁成功。 3. **锁误释放** - **问题**:客户端A释放了客户端B持有的锁。 - **解决方案**:释放锁时严格校验`unique_value`(如UUID)。 --- #### 四、**RedLock算法流程** 1. 获取当前时间戳$T_1$。 2. 向N个Redis实例依次发送加锁命令。 3. 计算加锁总耗时$T_{\text{total}}$: $$ T_{\text{total}} = \text{当前时间} - T_1 $$ 若$T_{\text{total}} < \text{锁过期时间}$,且半数以上实例加锁成功,则锁生效。 4. 若加锁失败,向所有实例发送释放锁命令。 --- #### 五、**适用场景** 1. 秒杀库存扣减 2. 定时任务防重复执行 3. 分布式事务协调 --- #### 六、**注意事项** 1. **避免长期锁**:锁过期时间需覆盖业务执行时间,但不宜过长。 2. **客户端唯一标识**:必须使用唯一值(如UUID)作为锁的值。 3. **性能权衡**:RedLock算法需多次网络通信,可能影响性能。 --- #### 七、**代码示例(Python)** ```python import redis import uuid client = redis.Redis() lock_key = "resource_lock" unique_id = str(uuid.uuid4()) # 加锁 lock_success = client.set(lock_key, unique_id, nx=True, px=30000) if lock_success: try: # 执行业务逻辑 print("Do something...") finally: # 释放锁(Lua脚本) script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ client.eval(script, 1, lock_key, unique_id) ``` --- #### 八、**总结** Redis分布式锁通过`SET NX PX`原子性操作实现简单高效的互斥控制,但需注意锁过期、主从同步等问题。对一致性要求高的场景,建议使用RedLock算法或结合ZooKeeper等强一致性系统。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值