(适合中国宝宝的小白)分布式锁应用

案例地址:
https://github.com/heixhei/demos

今天程序员小白被安排做一个秒杀的功能,小白迅速的打开了idea准备大展身手,不到几分钟,小白高兴的想把代码推送部署上去了,师傅看到小白这么开心,看了看小白的屏幕,气的脸色通红,直呼我没教过你这样的徒弟,我们瞧一瞧小白的又闹出了什么笑话。

线程不安全-导致超卖

小白看到秒杀后,迅速写下了如下代码:


/**
 * 线程不安全--导致超卖
 */
public class V1
{
    private static Long stock = 1L;

    /**
     * 用户下单
     */
    public static void placeOrder() throws InterruptedException {
        if (stock > 0) {
            Thread.sleep(100);
            stock--;
            System.out.println(Thread.currentThread().getName() + "秒杀成功");
        } else {
            System.out.println(Thread.currentThread().getName()+"秒杀失败,库存不足");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    placeOrder();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(stock);//stock=0
    }
}


我们看到小白犯了一个低级错误,没有考虑线程安全,导致商品超卖了。
在这里插入图片描述
用户对数据读取出现了脏读。如下图所示,所有用户可以同时对一个共有变量进行改写,造成了数据混乱。
在这里插入图片描述

同步锁-分布式集群导致超卖

小白脑袋转了转,对呀这不是典型的并发场景吗?几分钟后,小白重新写了新的代码,如下:


/**
 *  同步锁--分布式集群导致超卖
 */
public class V2 {
    private static Long stock = 1L;

    /**
     * 用户下单
     */
    public static void placeOrder() throws InterruptedException {
        synchronized (stock) {
            if (stock > 0) {
                Thread.sleep(100);
                stock--;
                System.out.println(Thread.currentThread().getName() + "秒杀成功");
            } else {
                System.out.println(Thread.currentThread().getName()+"秒杀失败,库存不足");
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                try {
                    placeOrder();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }).start();
        }
    }
}

为了以防万一,小白还做了压力测试,都没出现超卖问题了。师傅看到后,感慨道;”蟹bro,Do you know BAT?我们可是大厂,你要我们分布式集群怎么办?“,小白摸着脑袋问道:”分布式集群有什么关系吗?“

Redis原生实现分布式锁

是啊,分布式集群有什么关系吗?
下图,我们可以看到,当我们的一个线程对某一资源设置同步锁后,其他线程都需要等待其解锁后在争抢资源,随着用户量增加,该系统性能达到瓶颈,需要进行水平扩展。
在这里插入图片描述
我们用到了Nginx进行反向代理和负载均衡,经过压测后,虽然性能上来了,可这时又出现超卖问题。synchronized它不香吗为啥还要用分布式锁?
我们可以看到,由于同步锁属于JVM级别的锁,只能锁住当前进程内的资源,分布式集群部署下,又会出现脏读。小白深夜找资料学习,终于有了思路,使用Redis和Zookeeper实现分布式锁,因为项目中本身用到了Redis,所以我们使用Redis实现。浅析redis setIfAbsent的用法及在分布式锁上的应用及同步锁的缺陷
在这里插入图片描述
代码如下:

/**
 * redis原生实现分布式锁
 */
@SpringJUnitConfig(classes =RedisConfig.class)
public class V3 {
    public static Long stock = 1L;

    public static final String LOCK_KEY = "lock::productId";

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 下单
     */
    public void placeOrder() {
        //加上同步锁
        Boolean flag = redisTemplate.opsForValue()
                .setIfAbsent(LOCK_KEY, "1", 10, TimeUnit.SECONDS);
        try {
            if (flag) {
                if (Thread.currentThread().getName().equals("Thread-1")) {
                    throw new RuntimeException("人为异常");
                }
                if (stock > 0) {
                    Thread.sleep(100);
                    stock--;
                    System.out.println(Thread.currentThread().getName() + "秒杀成功");
                } else {
                    System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");
                }

            } else {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "重试");
                placeOrder();
            }
        } catch (Exception exception) {
            System.out.println(Thread.currentThread().getName()+"异常");
            exception.printStackTrace();
        }
        finally {
            redisTemplate.delete(LOCK_KEY);
        }
    }
    @Test
    public  void main() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(() -> {
                placeOrder();
            });
            thread.start();
            thread.join();
        }
    }

}

上述代码使用Redis原生的SETNX来实现分布式锁,当服务器访问Redis使用SETNX放回1时,表示该资源没有被占用,返回0时,表示该资源被占用,从达到加锁的效果。是不是很巧妙。
在这里插入图片描述

但我们要注意,

  1. 当锁过期时线程还在处理业务中
    
  2. 当处理完释放其他线程的锁
    

也就是线程-1业务还在执行,锁并没有释放,但已经过期了,其他线程就可以设立新的锁,当线程-1释放锁时,却是释放其他线程的锁
解决方法:

  1.  加长时间,并添加子线程每10秒确认线程是否在线,在线则将过期时间重设
    
  2.  给锁加唯一ID(UUID)
    

Redisson 实现分布式锁

看完这时,小白又想了想,这虽然是个好办法,但是逻辑有点复杂,而且还需要自己保证代码的健壮性太困难了,有没有现有的组件来实现呢?小白发现了Redisson提供了这个功能。代码如下:

/**
 * Redisson 实现分布式锁
 */
@SpringJUnitConfig(classes = {RedissonAutoConfiguration.class, RedisConfig.class})
public class V4 {
    public static Long stock = 1L;
    public static final String LOCK_KEY = "lock::productId";

    @Autowired
    private RedissonClient redisson;

    /**
     * 用户下单
     */
    public void placeOrder() {
        RLock lock = redisson.getLock(LOCK_KEY);
        lock.lock();
        try {
            //创建人为异常
            if (Thread.currentThread().getName().equals("Thread-2")) {
                throw new RuntimeException("人为异常");
            }
            if (stock > 0) {
                Thread.sleep(100);
                stock--;
                System.out.println(Thread.currentThread().getName() + "秒杀成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");
            }
        } catch (Exception ex) {
            System.out.println(Thread.currentThread().getName() + "异常:");
            ex.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println(stock);
        }
    }

    @Test
    public void main() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(() -> {
                placeOrder();
            });
            thread.start();
            thread.join();
        }
    }
}

下图是Redisson实现分布式锁的底层流程图:
在这里插入图片描述
但是Redis主从集群有一个问题,当资源加锁成功后,Master节点挂了怎么办?
因为Redis主从节点是AP架构,加锁后,不会立即同步给从节点,导致数据丢失,并没有加锁成功。小白这时又看到一个叫做RedLock的东西。

RedLock实现高可用分布式锁

RedLock使所有的Redis节点都完成加锁
代码如下:

/**
 * redLock 实现分高可用布式锁
 */
@SpringJUnitConfig(classes ={RedissonAutoConfiguration.class,RedisConfig.class} )
public class V5 {

    public static Long stock=1L;
    public static  final  String LOCK_KEY="lock::productId";

    @Autowired
    RedissonClient redisson;
    /**
     * 下单
     */
    public   void placeOrder() {

        // 这里需要不同的redssion客户端,配置连接到不同的redis服务器
        RLock lock = redisson.getLock(LOCK_KEY);
        RLock lock2 = redisson.getLock(LOCK_KEY);
        RLock lock3 = redisson.getLock(LOCK_KEY);

        //
        RedissonRedLock redissonRedLock = new RedissonRedLock(lock,lock2,lock3);

        redissonRedLock.lock(30,TimeUnit.SECONDS);
        try {
                if (Thread.currentThread().getName().equals("Thread-1")) {
                    throw new RuntimeException("人为异常!");
                }
                if (stock > 0) {
                    Thread.sleep(100);      //模拟执行业务...
                    stock--;
                    System.out.println(Thread.currentThread().getName() + "秒杀成功");
                } else {
                    System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");
                }
        } catch (Exception ex) {
            System.out.println(Thread.currentThread().getName() + "异常:");
            ex.printStackTrace();
        }
        finally {
            lock.unlock();
            System.out.println(stock);
        }
    }

    @Test
    public  void main() throws InterruptedException {
        for (int i=0;i<3;i++){
            Thread thread = new Thread(() -> {
                placeOrder();
            });
            thread.start();
            thread.join();
        }
    }
}

编写不易,求个三连
感谢徐庶老师讲解

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值