redis分布式锁—–实现秒杀的思路

  • Post author:
  • Post category:其他

工作中,有很多业务场景需要实现类似秒杀,抢购的功能,这种短时高并发的场景尤其需要注意防止商品出现超卖的问题,一旦超卖,各位猿们就准备删库跑路把~~~~

大家可能会想,synchronized大法!别,想想拉肚子时厕所满坑的尴尬~~~~

这时可以了解下用redis实现分布式锁,它能够实现分布式环境下的数据一致性,其本质是利用了redis是单线程的,或者说redis的网络模块是单线程的,其他模块还是用多线程的!

实现的思路主要依靠redis的两个原子性命令:SETNX     GETSET

SETNX:将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作

GETSET:将给定 key 的值设为 value ,并返回 key 的旧值(old value)

 

贴一下我的代码:


@RestController
@RequestMapping(value = "test")
public class MsService {

    @Autowired
    private RedisService redisService;


    //锁超时时间,防止线程在入锁以后,无限的执行等待
    private int expireMsecs = 3 * 1000;
    //锁等待时间,防止线程饥饿
    private int timeoutMsecs = 10 * 1000;
    private volatile boolean locked = false;
    //设置锁的唯一key
    String lockKey = "good";
    //模拟初始商品数
    int n = 50;






    /***
     * 抢购代码
     * @return
     */
    @RequestMapping(value = "/thread",method = RequestMethod.GET)
    public boolean seckill() {
        try {
            if(n<=0) {
                System.out.println("手慢拍大腿,抢光啦222222222!");
                return false;
            }
            if (this.lock()) {
                //修改库存
                if(n-1>=0) {
                    n--;
                    System.out.println("库存数量:"+n+"     成功!!!"+Thread.currentThread().getName() + "获得了锁");
                }else {
                    System.out.println("手慢拍大腿,抢光啦!");
                    return false;
                }
                this.unlock();
                return true;
            }else System.out.println("换个姿势再试试");


        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,
            // 操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做
            this.unlock();
        }
        return false;
    }


    public synchronized boolean lock() throws InterruptedException {

        //SETNX是将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
        //GETSET key value,将给定 key 的值设为 value ,并返回 key 的旧值(old value)
        //redis是单线程的,这里的单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程


            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); // 锁到期时间
            //满足下面的判断即该线程成功抢到了锁
            if (redisService.setnxByKey(lockKey, expiresStr).toString().equals("1")) {
                // lock acquired
                locked = true;
                return true;
            }
            //没有抢到锁的线程需要判断锁是否已经超时,若超时需要释放锁
            String currentValueStr = redisService.getByKey(lockKey); //当前获取锁的线程的到期时间
            //判断是否超时
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                // 判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
                // lock is expired

                //为当前锁设置新锁到期时间,并且获取到旧锁到期时间
                String oldValueStr = redisService.getSetByKey(lockKey, expiresStr);
                //此处GETSET为原子性操作(类似抛绣球),若两个线程AB同时到这里,只有一个线程能够获取到旧锁的到期时间(假设为A),
                //此时B获取到的值是旧锁的到期时间+A锁的到期时间
                //下面的判断只有A锁满足条件,但是A的到期时间会被B的到期时间所覆盖,因为时间相差很少,所以一般可以接受
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    locked = true;
                    return true;
                }
            }
        return false;
    }





    /**
     * Acqurired lock release.
     */
    public synchronized void unlock() {
        if (locked) {
            redisService.del(lockKey);
            locked = false;
        }
    }
}

具体的逻辑,注释已经很清楚了!这里没有加入数据库,大家自行加入,抢购成功时加入修改库存,查询库存的代码即可

最后,我加入了Jmeter进行测试,jmx文件我也已经上传到  ↓↓↓

全部代码在这:中国最大男性交友网站,欢迎Fork


版权声明:本文为wangtaojiushiwo原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。