基于Redis实现分布式锁 Java版本(经典案例)

  |   1 评论   |   3,524 浏览

    当今的互联网业务系统大都是分布式的,在需要访问共享资源的时候,如果不能很好的协调和控制访问,就会出现意想不到的并发问题,导致系统异常。

    分布式锁应运而生,由于使用某些资源时只能由一个进程来处理,其作用就是协调各进程访问共享资源的一种方式,以保护数据的一致性和完整性。

    比如,Job的执行,秒杀出货,农场蔬菜订单交易,本文中会结合农场交易中蔬菜订单交易过程中出现的问题和解决思路。


    知识点


    Redis是单进程的,其操作都是原子性的,即使大量并发,依然是串行执行。况且Redis单实例的情况下,性能依然非凡,能应付大部分的场景。

    为了实现这个锁,会使用Redis的几个命令

    setnx ,其使用和set类似,只是setnx设置 k, v 时,当key存在时其返回0,当key不存在时其返回1 。

    expire ,通过这个命令可以设置存在的key失效, 比如,expire mykey 2,就是mykey将在2秒以后自动失效。

    del,就是删除一个key的命令

    再介绍一个命令,incr,就是计数器,每次访问都加1 。


    其实使用Redis锁住资源的最简单的办法就是 setnx 创建k,v 值,再设置 expire 一个过期时间,因为key会在过期时间到达后自动消失,也就意味着锁自动解除。

    当获得锁的client使用完资源,释放锁时,只需要del掉刚才创建的key即可。


    但上面会存在2个风险点,一个是key没有成功的设置失效时间,导致key永不消失;另一个就是client会存在删除另一个client持有的锁。


    在使用Sentinel 的情况下会自动failover,Redis的Replication是异步的,假如在Master上setnx了一个key后,这时发生了failover,很可能slave还没有同步刚才设置的key,

    这样就会存在多个client获取锁的风险,锁的安全性破坏了。通过对value设置当前时间,每次获取锁前获取value值和当前时间比较看是否大于锁的有效时间,这个思路来解决锁一直被占用的问题。


    第二个clientA获取锁后,可能由于自身业务处理时间过长,超时后锁失效,锁被其他clientB获取;当clientA去释放锁时,如果不恰当处理可能会把ClientB持有的锁删除掉。

    如何正常做呢?先创建一个唯一数,对任何客户端,任何请求都是唯一的,假如:Long currentNa = System.nanoTime();

    1、clientA获取锁,setnx lock_key currentNa ,然后 expire lock_key 2 ,key 2秒后自动失效

    2、clientA释放锁时必须这样做,就是在删除key时,key存在且key的value是自身当初设置的value时,才可以删除;否则直接返回0不处理。

    if (currentNa.equals(jedis.get("lock_key"))) {
            jedis.del("lock_key");
    } else {
            return 0;
    }

    这里其实还可能有个问题,就是del时发生了异常,锁没有正常释放,在2秒内其他client是无法获取该锁的。

    有利有弊,各有取舍,在业务允许的情况下,目前我们是这样做的。


    经典案例


    在业务系统中,有个虚拟农场蔬菜交易市场,里面每天都有大量的用户进行蔬菜交易,获得了蔬菜后,可以作为礼物送给主播。

    有人利用按键精灵进行购买蔬菜,发现他每交易一笔,就获得了大约100笔数量的蔬菜,经查access log得知,基本每秒过来的请求都在 200个左右,

    瞬时对一笔订单发出了那么多请求,导致蔬菜超卖,我们就亏了。


    看了下代码蔬菜交易的流程大致是这样的:

    1、用户发出购买蔬菜请求

    2、对请求进行session校验,用户交易资格校验

    3、校验蔬菜订单状态

    4、校验用户账号是否有足够金豆

    5、扣除用户账号的金豆同时写入金豆流水日志

    6、给卖家账号里面加金豆

    7、给买家仓库里面加蔬菜

    8、修改订单交易状态


    正常情况下这些步骤在不到1秒内完成,一般不会出问题。当用户每秒200个请求过来时,在第8步未完成时,前面已经过来了N多请求,这样就导致了蔬菜超卖。

    有人说,卖就卖呗,反正他要付金豆。用户很鬼精的,他可能注册好多小号,小号种蔬菜,然后把每笔蔬菜价格标的极低,通过这种手段把蔬菜汇聚到一块。


    咋解决? 第3步还校验啥订单啊,直接修改订单状态,根据返回值判断是否继续交易,这样有个问题,如果后续用户金豆不够或入库异常,还要恢复订单状态。

    前端拦截ip,凡是对蔬菜交易操作的情况,比如1秒内同ip下只能过来一个,比如:Lua + Redis,对同一个ip进行限制。那万一用户用代理不断变化ip咋办?

    所以,当时的处理,在不改变原来代码流程的情况下,还是想对订单进行加锁来处理的。

    大概是这样处理的:

    1、用户购买蔬菜时先锁住订单

    2、蔬菜交易

    3、蔬菜交易成功/失败,解锁


    大致的代码简写如下,是个参考:

    //加锁
    public static long lock(String orderNo) {
        long currentNa = System.nanoTime();
        int ret = jedis.setnx(orderNo, currentNa);
        jedis.expire(orderNo,2);
        return (ret == 0) ? 0 : currentNa;
    }

    发现返回0时,表示此订单已加锁,不再处理。


    //解锁
    public static long unlock(String orderNo, Long currentNa) {
        if (currentNa.equals(jedis.get(orderNo))) {
            jedis.del(orderNo);
        } else {
            return 0;
        }
    }

    评论

    发表评论

    validate