菜鸟带你手撕分布式锁-redis,看完不会算我输

背景

今天是五一,决定不出去,在家里撸代码,今天学习redis,因而准备写个基于redis的分布式锁。因为本人属于菜鸟级别,在写的过程当中遇到各类问题,功夫不负有心人,终于搞定,若是发现实现有问题,欢迎指导,感谢.git

在单机时代,虽然不须要分布式锁,但也面临过相似的问题,只不过在单机的状况下,若是有多个线程要同时访问某个共享资源的时候,咱们能够采用线程间加锁的机制,即当某个线程获取到这个资源后,就当即对这个资源进行加锁,当使用完资源以后,再释放锁,其它线程就能够接着使用了。JAVA中已提供相关工具类。可是到了分布式系统的时代,这种线程或者进程之间的锁机制,就可能没做用了,系统可能会有多份而且部署在不一样的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。所以,为了解决这个问题,咱们就必须引入「分布式锁」。github

分布式锁,是指在分布式的部署环境下,经过锁机制来让多客户端互斥的对共享资源进行访问。web

通常分布式锁要知足一下几点要求:redis

  • 排他性:在同一时间只会有一个客户端能获取到锁,其它客户端没法同时获取
  • 避免死锁:这把锁在一段有限的时间以后,必定会被释放(正常释放或异常释放)
  • 高可用:获取或释放锁的机制必须高可用且性能佳

分布式锁实现方式

目前主流的分布式锁实现主要有如下几种数据库

  • 基于redis实现
  • 基于数据库实现
  • 基于zookeeper的实现

今天主要将基于redis实现分布式锁缓存

redis分布式锁实现

redis分布式锁基础知识

  • 缓存过时安全

    缓存能够设置过时时间,redis根据时间自动进行清理。并发

  • setNx命令dom

将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不作任何动做。
SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。
复制代码
  • lua脚本编辑器

    脚本语言,用于支持redis原子操做。

熟悉以上redis知识,实现redis分布式锁比较容易了。

关注点

  • 缓存过时

    最好给加锁的key设置缓存过时时间,能够有效的防止死锁,好比某个进程加锁后没来得及释放锁,宕机,说来负责释放锁?

  • set值

    加锁时,在redis中保存在各节点中惟一的值,防止不一样进程误解锁

好比serviceA已经在redis中加锁lock,通常serviceA执行时间为1秒,则设置缓存过时时间2秒,某天因为机器缘由serviceA执行了3秒,那么对应的锁已经失效,此期间B去加锁,并加锁成功, serviceA执行完会释放锁,致使serviceA会将B加的锁释放,因此产生误删锁,采用惟一值,避免这种状况产生。删锁会检查值,若是加锁与解锁的值不相同则不容许解锁。

  • 加锁与失效时间必需要原子性

核心逻辑

一、利用SETNX命令加锁

public static String set(String key, String value, long timeout) {
        Jedis jedis = getJedis();
        try {
            String ret = jedis.set(key, value, "NX", "PX", timeout);
            return ret;
        } finally {
            close(jedis);
        }
    }
复制代码

二、实现阻塞加锁和非阻塞加锁

/** * 阻塞加锁 */
    public void lock() {
        if (tryLock()) {
            return;
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //lock
        lock();
 }  /** * 基于setNx实现非阻塞锁 * * @return */ public boolean tryLock() { String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, DEFAULT_TIME_OUT); if ("OK".equals(ret)) { //lock success LOCAL.set(uuid); return true; } return false; }  public boolean tryLock(long time, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, unit.toMillis(time)); if ("ok".equals(ret)) { LOCAL.set(uuid); //lock success return true; } return false; } 复制代码

三、解锁

解锁的同时须要去检查值是否与加锁的值相同,不相同则不容许解锁,这里是经过ThreadLocal传加锁产生的uuid

/** * unlock * 执行lua脚本,保证原子性 */
    public void unlock() {
        release();
    }
 private void release() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; JedisUtis.remove(LOCK_KEY, script, LOCAL.get()); } 复制代码

场景验证

12306售票是并发学习中经典案例,仍是拿这个举例,好比有100 tickets,有多个售票窗口同时售票,怎么保证不被重复卖

/** * 线程不安全示例 * * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicket implements Runnable {
 private int tickets = 100;  public void run() { for (; ; ) { sale(); if (tickets < 0) break; } } }  /** * 售票 */ private void sale() { if (tickets > 0) { tickets--; System.out.println(Thread.currentThread().getName() + " - 在售第" + (100 - tickets) + "票 :: 剩余" + (tickets)); } try { Thread.sleep(100); } catch (InterruptedException e) {  } } } 复制代码

测试类

/** * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicketTest {
 BlockingDeque queue = new LinkedBlockingDeque(100);  private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 100, TimeUnit.SECONDS, queue);  @Test public void testSaleTickets() throws IOException { SaleTicket saleTicket = new SaleTicket(); executor.execute(new Thread(saleTicket, "售票员001")); executor.execute(new Thread(saleTicket, "售票员002")); executor.execute(new Thread(saleTicket, "售票员003")); executor.execute(new Thread(saleTicket, "售票员004")); System.in.read(); } } 复制代码

存在重复售票状况,改用redisLock,调整核心代码

public void run() {
        for (; ; ) {
            lock.lock();
            try {
                sale();
                if (tickets < 0) break;
            } finally {
                lock.unlock();
            }
        }
    }
复制代码

执行结果以下

完整代码已上传github.com/qiqsa/distr…

相关文章
相关标签/搜索