原创 蚂蚁金服面试:如何优雅的用Redis实现分布式锁?

发布时间:2021-06-24 15:13:37 浏览 314 来源:猿笔记 作者:麒麟改bug

    此时需要利用锁的技术控制某一时刻修改数据的进程数。-与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。至于利用数据库、文件等做锁与单机的实现是一样的,一个方法在同一时间只能被一个机器的一个线程执行;-高性能的获取锁与释放锁;即没有获取到锁将直接返回获取锁失败,当前锁的失效时间为10s,如果当前扣减库存的业务逻辑执行需要15s时,线程1删除线程2加的锁(product\\_001),当前线程删除当前线程所加的锁**。2这样能保证每个线程删除的锁为当前线程添加的锁,此时线程2则会加锁成功,定时去查看**是否存在主线程的持有当前锁**。


    # * *一、分布式锁简介* *

    ##**1.什么是分布式锁* *

    -当分布式模型下只有一个数据副本(或限制)时,需要使用锁技术来控制某一时刻修改数据的进程数。

    -单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。

    -分布式锁仍然可以在内存中存储标签,但是内存不是某个进程分配的内存,而是Redis、Memcache等公共内存。至于数据库、文件等锁的使用,和单机实现一样,只要标签可以互斥即可。

    ##**2.分布式锁的条件* *

    -在分布式系统环境中,一个方法只能由机器的一个线程同时执行;

    -高度可用的获取和释放锁;

    -高性能锁获取和释放;

    -具有折返特征;

    -具有锁失效机制以防止死锁;

    -具有非阻塞锁特性,即不获取锁,直接返回获取锁失败。

    # * *二。用Redis实现分布式锁* *

    ##**1.通用代码实现* *

    javascript@RequestMapping("/deduct_stock")publicStringdeductStock(){StringlockKey="product_001";try{/*Booleanresult=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"aaa");//jedis.setnxstringRedisTemplate.expire(lockKey,30,TimeUnit.SECONDS);//设置超时*///为解决原子性问题将设置锁和设置超时时间合并Booleanresult=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"aaa",10,TimeUnit.SECONDS);//未设置成功,当前key已经存在了,直接返回错误if(!result){return"error_code";}//业务逻辑实现,扣减库存....}catch(Exceptione){e.printStackTrace();}finally{stringRedisTemplate.delete(lockKey);}return"end";}

    ##**2.问题分析* *

    从上面的代码可以看出,当前锁的到期时间是10s。如果目前扣库存的业务逻辑需要15s,那么高并发会出现问题:

    -线程1,首先执行到10s后,锁(product\\_001)失效

    -线程2也在10s之后进入当前方法,它添加了一个锁(product \u 001)

    -当执行达到15s时,线程1删除线程2添加的锁(产品\\_001)

    -线程3,可以添加锁...如果进行了这个循环,那么实际的锁是没有意义的。

    **a)方案1:当前线程删除当前线程添加的锁* *

    javascript@RequestMapping("/deduct_stock")publicStringdeductStock(){StringlockKey="product_001";//定义唯一的客户端IDStringclientId=UUID.randomUUID().toString();try{//为解决原子性问题将设置锁和设置超时时间合并,将clientID作为值放入锁中Booleanresult=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);//未设置成功,当前key已经存在了,直接返回错误if(!result){return"error_code";}//业务逻辑实现,扣减库存....}catch(Exceptione){e.printStackTrace();}finally{//只有在获取锁的值为当前clientId时才会进行删除锁操作if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){stringRedisTemplate.delete(lockKey);}}return"end";}

    这样可以保证每个线程删除的锁都是当前线程添加的锁,但是仍然会存在超售的问题:当线程1还没有执行完,锁已经到了到期时间,那么线程2就锁定成功了

    **b)方案二:续命锁* *

    定义一个子线程,定期检查主线程是否持有当前锁,如果存在则延长到期时间。

    **c)方案3:Redisson**

    javascript@AutowiredRedissonredisson;@RequestMapping("/deduct_stock_redisson")publicStringdeductStockRedisson(){StringlockKey="product_001";RLockrlock=redisson.getLock(lockKey);try{rlock.lock();//业务逻辑实现,扣减库存....}catch(Exceptione){e.printStackTrace();}finally{rlock.unlock();}return"end";}

    -多线程执行锁操作,只有一个线程可以锁成功,其他线程循环阻塞。

    -锁定成功,锁定超时默认为* * 30s * *,后台线程启动。被锁定的后台每隔10s * *会* *检查线程持有的锁是否存在,如果仍然存在,则延迟锁超时并重置为30s,即* *延迟锁。

    -对于原子性,Redis分布式锁底层通过**Lua脚本实现锁的原子性。锁扩展由底层的Lua延迟,延迟检测时间为timeout/3

    #**三、采用Redisson分布式锁的问题分析**

    ##**1.主从同步问题* *

    当主Redis锁定时,它开始执行线程。如果锁没有通过异步同步与从Redis节点同步,主节点将挂起。此时,一个从节点将作为新的主节点。这个时候其他线程可以锁定,所以出了问题。我该怎么办?

    a)采用zookeeper代替Redis

    由于zk集群的特点,支持CP。Redis集群支持AP。

    b)采用RedLock

    假设有三个redis节点,既没有主从关系,也没有集群关系。客户端用相同的密钥和随机值在三个节点上请求锁,请求锁的超时应该小于锁的自动释放时间。当在2个(超过一半)redis上请求锁时,锁是真正获得的。如果没有获得锁定,部分锁定的redis将被释放。

    javascript@RequestMapping("/deduct_stock_redlock")publicStringdeductStockRedlock(){StringlockKey="product_001";//TODO这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了RLockrLock1=redisson.getLock(lockKey);RLockrLock2=redisson.getLock(lockKey);RLockrLock3=redisson.getLock(lockKey);//向3个redis实例尝试加锁RedissonRedLockredLock=newRedissonRedLock(rLock1,rLock2,rLock3);booleanisLock;try{//500ms拿不到锁,就认为获取锁失败。10000ms即10s是锁失效时间。isLock=redLock.tryLock(500,10000,TimeUnit.MILLISECONDS);System.out.println("isLock="+isLock);if(isLock){//业务逻辑处理...}}catch(Exceptione){}finally{//无论如何,最后都要解锁redLock.unlock();}}

    具体使用有争议,不推荐。推荐Redisson用于高可用性并发,推荐zookeeper用于一致性。

    ##**2.提高并发性:分段锁* *

    事实上,Redisson将并行请求转换为串行请求。这降低了并发响应速度。为了解决这个问题,可以对锁进行分段:例如,原本有1000件商品的killing goods 001,可以分成20段,每段分配50件商品...

    * *以上是关于Redis分布式锁的* * * *学习笔记* *]

作者信息

麒麟改bug [等级:3] 公众号:麒麟改bug
发布了 58 篇专栏 · 获得点赞 290 · 获得阅读 13317

相关推荐 更多