3.秒杀模块-分布式加锁问题-如何利用Redis分布式锁实现控制并发

redis命令解释

说道Redis的分布式锁都是通过setNx命令结合getset来实现的,在讲之前我们先了解下setNx和getset的意思,在redis官网是这样解释的 
注:redis的命令都是原子操作

SETNX key value(加入没有则设置,有则不设置了)

将 key 的值设为 value ,当且仅当 key 不存在。 
若给定的 key 已经存在,则 SETNX 不做任何动作。 
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。 
可用版本: 
1.0.0+ 
时间复杂度: 
O(1) 
返回值: 
设置成功,返回 1 。 
设置失败,返回 0 。

redis> EXISTSjob                # job 不存在

(integer) 0

redis> SETNX job "programmer"    # job 设置成功

(integer) 1

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败

(integer) 0

redis> GET job                   # 没有被覆盖

"programmer"

GETSET key value(获取旧值再设置新值)

将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 
当 key 存在但不是字符串类型时,返回一个错误。 
可用版本: 
1.0.0+ 
时间复杂度: 
O(1) 
返回值: 
返回给定 key 的旧值。 
当 key 没有旧值时,也即是, key 不存在时,返回 nil 。

redis> GETSET dbmongodb    # 没有旧值,返回 nil

(nil)

redis> GET db

"mongodb"

redis> GETSET dbredis      # 返回旧值 mongodb

"mongodb"

redis> GET db

"redis"

代码示例

注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 
我们看下代码涉及以下几个类,这里有关业务逻辑相关的只定义了方法没有具体实现,关键是学习思路 ,还有里面的红色字体的备注。
 
OrderBiz.java

/**

 * 使用redis锁来控制并发抢单

 * @author fuyuwei

 */

publicclassOrderBiz {

    publicintcreateOrder(){

        // 下单之前的参数、合法性校验这里就不在演示

        OrderLock<Boolean> orderLock = new RedisOrderLock<Boolean>("pro-12345678901");

        boolean isSyn = orderLock.isSyn(new OrderLockBiz<Boolean>(){

            @Override

            public Boolean createOrder() {

                // 省去创建订单逻辑

                returnnull;

            }

        });

        if(!isSyn){

            BizLogger.info("创建订单失败");

        }

        return0;

}

·         

OrderLock.java

publicinterface OrderLock<T> {

    public boolean isSyn(OrderLockBiz<T> orderBiz);

 

}

OrderLockBiz.java

publicinterface OrderLockBiz<T> {

    public T createOrder();

}

RedisOrderLock.java

public classRedisOrderLock<T> implementsOrderLock<T> {

 

    //  锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会

    public static final long timeout = 10000;//ms

 

    // 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放

    public static final long expireMsecs = 10000;// ms

 

    public String lockKey = "orderLockKey";

 

    public Jedis jedis;

 

    private static volatile JedisPool jedisPool;

 

    public RedisOrderLock(String lockKey) {

        this.lockKey = lockKey;

    }

    /**

     * 初始化redis

     * @return

     */

    public Jedis getInstance() {

        if(jedisPool == null) {

            synchronized(RedisOrderLock.class) {

                if(jedisPool == null) {

                    JedisPoolConfig config = new JedisPoolConfig();

                    config.setMaxIdle(100);

                    jedisPool = new JedisPool(config,"localhost",6379, 3000,"test");

                }

            }

        }

        return jedisPool.getResource();

    }

 

    /**

     * 线程安全的业务逻辑处理

     */

    @Override

    public boolean isSyn(OrderLockBiz<T>orderBiz) {

        jedis = this.getInstance();

        try {

            // 获取到锁

            if(acquire(jedis)){

                // 执行创建订单逻辑

                orderBiz.createOrder();

            }else{

                BizLogger.info("waiting other thread creating");

            }

        } catch (Exception e) {

            BizLogger.error(e,"acquire lock failre");

        }finally{

            // 解锁

            this.releaseLock(jedis);

        }

        returnfalse;

    }

 

    /**

     * accqure lock

     * @param jedis

     * @return

     * @throws InterruptedException

     */

    public synchronized boolean acquire(Jedisjedis){

        boolean locked = false;

        while(timeout > 0){

            long expires =System.currentTimeMillis() + expireMsecs + 1;

            // 10秒之后锁到期

            String expiresStr =String.valueOf(expires);

            // 获取到锁

            if(jedis.setnx(lockKey, expiresStr) == 1){

                locked = true;

                return locked;

            }

            // 没有获取到锁,获得old

            String oldValue =jedis.get(lockKey);

            // expireMsecs10秒)锁的有效期内无法进入if判断,如果锁超时了

            if(oldValue != null

                    &&Long.parseLong(oldValue) < System.currentTimeMillis()){

                // 如果锁超时重新设置(则在这可以获得新锁)

                String oldValue_ =jedis.getSet(lockKey, expiresStr);

                // 值相同说明是同一个线程的操作,获取锁成功(把redis里的锁头set为新值,但是get还是得到旧值。所以可以对比,比如A先设置,则取到旧的,设置A的,若BA后面,则B设置了,但是去到A的值,所以可以做下面的判断。取到旧的才是取得锁头成功。)

                if(Long.valueOf(oldValue_) ==Long.valueOf(oldValue)){

                    locked = true;

                }else{

                    // 被其他线程抢先获取锁

                    locked = false;

                }

            }

            // 锁没有超时,继续等待

            returnfalse;

        }

    }

    /**

     * 释放锁

     * @param jedis

     */

    public synchronized void releaseLock(Jedisjedis){

        try {

            long current =System.currentTimeMillis(); 

            // 避免删除非自己获取得到的锁

            if (current <Long.valueOf(jedis.get(lockKey)))

                jedis.del(lockKey);

        } catch (Exception e) {

            e.printStackTrace();

        }finally{

            // 把用完的连接放到连接池汇中供其他线程调用

            jedisPool.returnResource(jedis);

        }

    }

}

 

但是原先会有这种问题:

1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。

3. 锁不具备拥有者标识,即任何客户端都可以解锁。

 

publicclass RedisTool {

 

    privatestaticfinal String LOCK_SUCCESS = "OK";

    privatestaticfinal String SET_IF_NOT_EXIST = "NX";

    privatestaticfinal String SET_WITH_EXPIRE_TIME ="PX";

 

    /**

     * 尝试获取分布式锁

     * @param jedisRedis客户端

     * @paramlockKey

     * @paramrequestId 请求标识

     * @param expireTime 超期时间

     * @return是否获取成功

     */

    publicstaticboolean tryGetDistributedLock(Jedis jedis, StringlockKey, String requestId, int expireTime) {

 

        String result = jedis.set(lockKey,requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

 

        if(LOCK_SUCCESS.equals(result)) {

            returntrue;

        }

        returnfalse;

 

    }

 

}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

·     第一个为key,我们使用key来当锁,因为key是唯一的。

·     第二个为value,我们传的是requestIduserid),很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

·     第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

·     第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

·     第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。(解决问题12)2. 已有锁存在,不做任何操作。

1.   set keyproductidvalueuserid,设置过期时间。解决要求客户端时间一样和getset问题(解决问题12)

2.    解锁要先验证userid,如果不是,则业务回滚;如果是,则解锁;即谁去解锁,要它的锁才能给他解,防止很多客户端都可以解锁,使用这种方式释放锁可以避免别的客户端获取成功的锁被删除。(解决问题34)(这个有点类似CAS操作。)

举个例子:客户端A取得资源锁,但是紧接着被一个其他操作阻塞了,当客户端A运行完毕其他操作后要释放锁时,原来的锁早已超时并且被Redis自动释放,并且在这期间资源锁又被客户端B再次获取到。如果仅使用DEL命令将key删除,那么这种情况就会把客户端B的锁给删除掉。

附(原来的3个问题)

1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。

3. 锁不具备拥有者标识,即任何客户端都可以解锁。

4. 关于过期时间的设置,如果时间设置得太短。有可能某线程挂起导致锁过期了没有锁,还导致线程成功操作后。

 

残留的未解决的问题:公平性和过期时间的设置。

1.      使用一个消息队列,像IO多路复用那种思想可以解决公平性;

2.      可以使用zookeeper解决;在底层,给每个线程分节点ID,从小排序到大。

过期时间长度,需要怎么权衡,暂时还没有想到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值