首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

1.互斥性。在任意时刻,只有一个客户端能持有锁。
2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
。

应用场景:订单模块新增一条订单,商品模块减少库存,共用一个数据库

不加锁代码:

/**
     * 减少库存-不加锁-会存在超卖问题
     *
     * @param productId
     * @return
     */
    @Transactional
    @Override
    public Pair<Boolean, String> reductStock(Integer productId) {
        Integer stock = this.productMapper.getStock(productId);
        if (stock <= 0) {
            return Pair.of(false, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】库存不足,剩余库存{0}", stock));
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.productMapper.reductStock(productId);
        return Pair.of(true, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】减少库存成功,剩余库存{0}", this.productMapper.selectByPrimaryKey(productId).getStock()));
    }

<select id="getStock" resultType="java.lang.Integer">
         select stock from product where id=#{productId}
</select>
<update id="reductStock">
        update product set stock=stock-1 where id=#{productId}
</update>

 

方式一(数据库-for update悲观锁):

        /**
         * 总结几个点:
         * 1.for update 锁必须使用事务,不然起不到真正锁的效果,数据库默认的写语句都是执行即提交
         * 2.for update 锁是行锁,查询语句必须走主键索引,否则可能锁掉其他数据或者整个表
         * 3.for update加锁后,其他线程或进程中数据库客户端执行语句和java客户端执行语句都会进入等待状态
         * 4.for update加锁后,还没来得及解锁程序异常时,事务会回滚,释放锁
         */

/**
     * 减少库存-数据库for update锁-悲观锁
     *
     * @param productId
     * @return
     */
    @Transactional
    @Override
    public Pair<Boolean, String> reductStockByLock(Integer productId) {
        //加锁
        Integer stock = this.productMapper.getStockByLock(productId);
        logger.info("【线程" + Thread.currentThread().getId() + "】加锁成功。。。");
        if (stock <= 0) {
            return Pair.of(false, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】库存不足,剩余库存{0}", stock));
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //如果加锁后程序遇到异常,事务会释放当前锁资源
        //Integer num = 1 / 0;
        //解锁
        this.productMapper.reductStock(productId);
        logger.info("【线程" + Thread.currentThread().getId() + "】解锁成功。。。");
        return Pair.of(true, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】减少库存成功,剩余库存{0}", this.productMapper.selectByPrimaryKey(productId).getStock()));
    }
<select id="getStockByLock" resultType="java.lang.Integer">
        select stock from product where id=#{productId} for update
</select>

方式一(数据库-乐观锁):

/**
     * 减少库存-数据库version锁-乐观锁
     *
     * @param productId
     * @return
     */
    @Transactional
    @Override
    public Pair<Boolean, String> reductStockByVersionLock(Integer productId) {
        Product product = this.productMapper.selectByPrimaryKey(productId);
        Integer stock = product.getStock();
        String version = product.getLockVersion();
        if (product.getStock() <= 0) {
            return Pair.of(false, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】,版本:" + version + ",库存不足,剩余库存{0}", stock));
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //更新的时候条件where要带上version
        Integer result = this.productMapper.reductStockByVersionLock(productId, version);
        if (null == result || result <= 0) {
            return Pair.of(true, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】,版本:" + version + ",减少库存失败,剩余库存{0}", this.productMapper.selectByPrimaryKey(productId).getStock()));
        }
        return Pair.of(true, MessageFormat.format("【线程" + Thread.currentThread().getId() + "】,版本:" + version + ",减少库存成功,剩余库存{0}", this.productMapper.selectByPrimaryKey(productId).getStock()));
    }
<update id="reductStockByVersionLock">
       update product set stock=stock-1,lock_version=lock_version+1 where id=#{productId} and lock_version=#{version}
</update>

方式二(Redis-Lua实现):

错误版本:

/**
     * 加锁
     *
     * @param lockKeyEnum 锁类型
     * @param lockDataId  锁对应的业务表数据id
     */
    public Boolean lock(LockKeyEnum lockKeyEnum, String lockDataId) {
        Jedis jedis = null;
        String lockKey = lockKeyEnum.getCode() + "_" + lockDataId;
        try {
            jedis = jedisPool.getResource();
            if (null == jedis) {
                return false;
            }
            //1表示成功 0表示失败(该key已存在)
            if (jedis.setnx(lockKey, lockDataId).equals(0)) {
                return false;
            }
            jedis.expire(lockKey, 10);
            return true;
        } catch (Exception e) {
            logger.error("加锁异常。。。", e);
            //手动解锁
            this.unLock(jedis, lockKeyEnum, lockDataId);
        } finally {
            //释放资源
            jedis.close();
        }
        return false;
    }


    /**
     * 解锁
     *
     * @param lockKeyEnum 锁类型
     * @param lockDataId  锁对应的业务表数据id
     */
    public Boolean unLock(Jedis jedis, LockKeyEnum lockKeyEnum, String lockDataId) {
        jedis.del(lockKeyEnum.getCode() + "_" + lockDataId);
        return true;
    }

错误点1(造成死锁):

加锁时:

如果执行到jedis.setnx整个程序挂掉了,这里指的是程序直接退出,try catch没有用,会导致死锁

改进1:

这里让判断赋值和设置过期时间一起执行,原子性操作

 

错误点2(删除掉别人的锁):

解锁时:

假设这里有ABC三个客户端请求排队,这时A客户端抢到了锁设置过期时间为10秒钟,如果这里执行的时间比较长,当第11秒钟的时候,(客户端A的)锁过期,

客户端B马上抢到锁并上锁,这时客户端A恰好执行完成,进行解锁操作,就会把(客户端B的)锁删除,这时客户端B处于裸奔状态,客户端C马上抢到锁并上锁,

这时问题来了,客户端B和客户端C的操作不是原子性,会出现互相抢资源,出现超卖的情况出现

改进2

给每一次客户端请求都加上version版本标记,解锁的时候判断是不是自己客户端请求的锁

加锁时:

解锁时:

 

错误点3(非原子操作删除掉别人的锁):

改进了2次后,似乎没问题了,但其实还是有一个隐藏的问题,看这段解锁代码,

假设客户端A加锁后,正常执行完成,在解锁的时候,判断版本这一句代码执行完的时候,(客户端A的)锁正好过期,然后(客户端B的)锁加上,

客户端A的解锁操作继续执行,这样客户端B的锁就被删除掉,客户端B就变成裸奔状态了

改进3

为了解决解锁时不是原子性操作的问题,解锁时必须引入lua语法来保证该操作是原子性的操作

所以最终的代码是:

package com.study.utils;

import com.study.enums.LockKeyEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Collections;
import java.util.UUID;


@Component
public class RedisLockByLua {

    private static final Logger logger = LoggerFactory.getLogger(RedisLockByLua.class);


    /**
     * 加锁-返回状态
     */
    private static final String LOCK_SUCCESS = "OK";

    /**
     * 解锁-返回状态
     */
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 设置-如果不存在就添加
     */
    private static final String SET_IF_NOT_EXIST = "NX";
    /**
     * 设置-过期时间
     */
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 过期时间(10秒钟)
     */
    private static final Long EXPIRE_TIME = 20 * 1000L;

    /**
     * 加锁
     *
     * @param lockKeyEnum
     *         锁类型
     * @param lockDataId
     *         锁对应的业务表数据id
     * @param lockVersion
     *         锁对应的版本(UUID)
     */
    public Boolean lock(Jedis jedis, LockKeyEnum lockKeyEnum, String lockDataId, String lockVersion) {
        String lockKey = lockKeyEnum.getCode() + "_" + lockDataId;
        try {
            while (true) {
                String result = jedis.set(lockKey, lockVersion, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                if (LOCK_SUCCESS.equals(result)) {
                    //加锁成功,跳出循环
                    return true;
                }
                logger.info("【线程{}】【版本:{}】等待加锁。。。", Thread.currentThread().getId(), lockVersion);
            }
        } catch (Exception e) {
            logger.error("【线程{" + Thread.currentThread().getId() + "}】【版本:{" + lockVersion + "}】加锁异常。。。", e);
            //手动解锁
            this.unLock(jedis, lockKeyEnum, lockDataId, lockVersion);
            return false;
        }
    }

    /**
     * 解锁
     *
     * @param lockKeyEnum
     *         锁类型
     * @param lockDataId
     *         锁对应的业务表数据id
     * @param lockVersion
     *         锁对应的版本
     */
    public Boolean unLock(Jedis jedis, LockKeyEnum lockKeyEnum, String lockDataId, String lockVersion) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKeyEnum.getCode() + "_" + lockDataId), Collections.singletonList(lockVersion));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

方式三(Redission实现):

这里有一篇文章详解了Redisson原理机制,大概Redisson内部加锁和解锁都用了Lua实现,保证原子性操作

https://blog.csdn.net/asd051377305/article/details/108384490

 /**
     * 减少库存-Redis-Redisson锁
     *
     * @param productId
     * @return
     */
    @Override
    public Pair<Boolean, String> reduceStockByRedisson(Integer productId) throws Exception {
        RLock rLock = null;
        try {
            //1.配置
            String address = "redis://" + REDIS_HOSTNAME + ":" + REDIS_PORT;
            Config config = new Config();
            config.useSingleServer().setAddress(address).setDatabase(0);
            //2.构建RedissonClient
            RedissonClient redissonClient = Redisson.create(config);
            String lockKey = LockKeyEnum.Product_Stock.getCode() + "_" + productId;
            rLock = redissonClient.getLock(lockKey);
            //等待获取锁的最大等待时间,超过这个值,则认为获取锁失败
            Long waitTimeOut = 30L;
            //锁的超时时间,超过这个时间锁会自动失效(设置的时间比业务处理时间大,保证有效期内业务能处理完成)
            Long leaseTime = 10L;
            logger.info("【线程{}】准备获取锁。。。", Thread.currentThread().getId());
            /**
             * 1.获取不到锁时会阻塞,内部实现了while(true)循环
             * 2.1如果我们指定了锁的超时时间 leaseTime,就发送给redis执行脚本,进行占锁,默认超时就是我们制定的时间,不会自动续期;
             * 2.2如果我们未指定锁的超时时间 leaseTime,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
             * 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动再续成30秒;
             */
            //3.加锁
            //Boolean result = rLock.tryLock(waitTimeOut, TimeUnit.SECONDS);
            Boolean result = rLock.tryLock(waitTimeOut, leaseTime, TimeUnit.SECONDS);
            if (result) {
                logger.info("【线程{}】获取锁成功,开始业务。。。", Thread.currentThread().getId());
                Product product = this.productMapper.selectByPrimaryKey(productId);
                Integer stock = product.getStock();
                if (stock <= 0) {
                    return Pair.of(false, MessageFormat.format("【线程{0}】库存不足,剩余库存{1}", Thread.currentThread().getId(), stock));
                }
                Thread.sleep(5000);
                this.productMapper.reductStock(productId);
                logger.info("【线程{}】业务执行完成。。。", Thread.currentThread().getId());
                return Pair.of(true, MessageFormat.format("【线程{0}】减少库存成功,剩余库存{1}", Thread.currentThread().getId(), this.productMapper.selectByPrimaryKey(productId).getStock()));
            } else {
                logger.info("【线程{}】获取锁失败。。。", Thread.currentThread().getId());
                return Pair.of(false, MessageFormat.format("【线程{0}】获取锁失败。。。", Thread.currentThread().getId()));
            }
        } catch (Exception e) {
            logger.error("【线程" + Thread.currentThread().getId() + "】减少库存异常。。。", e);
            throw e;
        } finally {
            //4.最后都要解锁(判断1.解锁状态2.解锁当前线程的锁)
            if (null != rLock && rLock.isLocked() && rLock.isHeldByCurrentThread()) {
                logger.info("【线程{}】解锁。。。", Thread.currentThread().getId());
                rLock.unlock();
            }
        }
    }

方式四(Zookeeper实现):

package com.study.utils;


import com.study.enums.LockKeyEnum;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;


public class ZookeeperLock implements Lock {

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        //ZookeeperLock lock = new ZookeeperLock(LockKeyEnum.Product_Stock.getCode() + "_1");
        //lock.lock();
        ////...执行业务
        //Thread.sleep(1000);
        //lock.unlock();
    }


    private static final Logger logger = LoggerFactory.getLogger(ZookeeperLock.class);

    @Value("${zookeeper.url}")
    private String zkUrl;
    /**
     * 锁-key
     */
    private String lockKey;

    /**
     * zk客户端
     */
    private ZkClient zk;


    /**
     * 客户端连接服务器超时时间(毫秒)
     */
    private static final Integer sessionTimeOut = 30000;

    /**
     * 计数器
     */
    private CountDownLatch latch;


    /**
     * 锁-根目录地址
     */
    private static final String rootNodePath = "/myLock";

    /**
     * 当前锁节点
     */
    private String nowNodePath;

    /**
     * 当前锁节点 等待释放的前一个节点
     */
    private String waitNodePath;


    public ZookeeperLock(String _lockKey) {
        this.lockKey = _lockKey;
        try {
            zk = new ZkClient(StringUtils.isEmpty(zkUrl) ? "192.168.247.129:2181" : zkUrl);
            if (!zk.exists(rootNodePath)) {
                //创建根节点(持久节点)
                zk.createPersistent(rootNodePath);
            }
        } catch (Exception e) {
            logger.error("初始化zk客户端异常。。。", e);
            if (null != zk) {
                zk.close();
            }
        }
    }


    /**
     * 获取锁
     */
    @Override
    public void lock() {
        if (this.tryLock()) {
            logger.info("【线程{}】获取锁成功。。。", Thread.currentThread().getId());
            return;
        } else {
            //等待锁
            this.waitForLock();
            //递归重新尝试获取锁,一旦监听到上一个锁线程释放了锁,再次重新获取锁
            logger.info("【线程{}】重新尝试获取锁。。。", Thread.currentThread().getId());
            lock();
        }
    }

    /**
     * 等待获取锁
     *
     * @return
     */
    private Boolean waitForLock() {
        try {
            logger.info("【线程{}】等待获取锁。。。", Thread.currentThread().getId());
            //注册监听此节点的变化
            IZkDataListener iZkDataListener = new IZkDataListener() {
                @Override
                public void handleDataChange(String dataPath, Object data) throws Exception {

                }

                @Override
                public void handleDataDeleted(String dataPath) throws Exception {
                    logger.info("【线程{}】【节点{}】监控到被释放删除。。。", Thread.currentThread().getId(), dataPath);
                    if (null != latch) {
                        latch.countDown();
                    }
                }
            };
            //子节点加入Watcher监控
            zk.subscribeDataChanges(waitNodePath, iZkDataListener);
            if (zk.exists(waitNodePath)) {
                logger.info("【线程" + Thread.currentThread().getId() + "】【节点{}】阻塞,【节点{}】等待解锁。。。", nowNodePath, waitNodePath);
                this.latch = new CountDownLatch(1);
                //这里阻塞,一直等待其他线程释放锁
                this.latch.await();
                logger.info("【线程" + Thread.currentThread().getId() + "】【节点{}】阻塞解除,【节点{}】解锁。。。", nowNodePath, waitNodePath);
            }
            //子节点释放Watcher监控
            zk.unsubscribeDataChanges(waitNodePath, iZkDataListener);
            return true;
        } catch (Exception e) {
            logger.error("等待获取锁异常。。。", e);
            throw new RuntimeException("等待获取锁异常。。。", e);
        }
    }

    /**
     * 它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)
     *
     * @return
     */
    @Override
    public boolean tryLock() {
        try {
            String splitStr = "_";
            //这个判断很重要,如果第一次进来tryLock,当前线程节点未指定,创建当前节点,不判断的话就会死循环,当前线程节点永远为最新产生的+1的节点
            if (StringUtils.isEmpty(nowNodePath)) {
                nowNodePath = zk.createEphemeralSequential(rootNodePath + "/" + lockKey + splitStr, "lock");
            }
            //获取所有子节点
            List<String> allChildrenList = zk.getChildren(rootNodePath);
            //筛选出当前业务锁对应的子节点
            List<String> nowChildrenList = allChildrenList.stream().filter(a -> a.contains(lockKey)).collect(Collectors.toList());
            //排序,升序
            Collections.sort(nowChildrenList);
            //节点内容:  product_stock_1_0000000001
            //如果当前锁节点是最小的节点
            if (nowNodePath.equals(rootNodePath + "/" + nowChildrenList.get(0))) {
                logger.info("【线程:{}】【节点:{}】为最小节点,获取到锁", Thread.currentThread().getId(), nowNodePath);
                return true;
            }
            logger.info("【线程:{}】【节点:{}】不是最小节点,等待获取锁", Thread.currentThread().getId(), nowNodePath);
            //如果当前锁节点不是最小的节点,找到比自己小1的节点
            List<String> smallerNodeList = nowChildrenList.stream().filter(a -> Long.valueOf(a.substring(a.lastIndexOf(splitStr) + 1)) < Long.valueOf(nowNodePath.substring(nowNodePath.lastIndexOf(splitStr) + 1))).collect(Collectors.toList());
            waitNodePath = rootNodePath + "/" + smallerNodeList.get(smallerNodeList.size() - 1);
        } catch (Exception e) {
            logger.error("尝试获取锁异常。。。", e);
            throw new RuntimeException("尝试获取锁异常。。。", e);
        }
        return false;
    }

    /**
     * 这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
     *
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        try {
            //删除当前节点
            if (zk.exists(nowNodePath)) {
                zk.delete(nowNodePath);
            }
            nowNodePath = null;
            //释放zk客户端连接
            zk.close();
            logger.info("【线程{}】解锁成功。。。", Thread.currentThread().getId());
        } catch (Exception e) {
            logger.error("解锁异常。。。", e);
            throw new RuntimeException("解锁异常。。。", e);
        }
    }

    /**
     * 返回绑定到此 Lock 实例的新 Condition 实例
     *
     * @return
     */
    @Override
    public Condition newCondition() {
        return null;
    }

    /**
     * 如果当前线程未被中断,则获取锁,可以响应中断
     *
     * @throws InterruptedException
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

}




简单总结:

从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper

从性能角度(从高到低)缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

更多推荐