【Java分析】解决秒杀问题的几种实现,使用分布式锁和redis事务实现的对比

秒杀功能最重要的就是对库存的把控性,所以也就是说一定要让查询修改库存这两个步骤具有原子性。

所以根据我的理解,可以得出以下几种解决方案:

1、使用数据库事务 2、加入Spring声明式事务 3、使用分布式锁 4、使用Redis事务

 

解决方案一:

     使用mysql事务,由于这种方法对于mysql压力太大,所以就不予以代码说明了。

解决方案二:

     Spring的话,可以加入@Transaction事务注解,不过依旧不是最佳选择。

解决方案三:

     分布式锁,可以使用Redis做这个功能,原理就是redis中有setnx这个功能 那么就是当key没有出现过的时候才能插入。所以就可以让并发请求先尝试setnx,成功的话就代表获取锁成功,然后操作减库存等操作,之后再删除掉key,让其他请求继续获取锁。

可能出现的问题:

1、获得锁之后线程挂了,永久不释放锁了。

答:设置锁的过期时间,即使某线程获得了锁之后挂了,也能在规定时间后释放锁

2、获得锁之后线程由于网络延迟或者别的原因卡住了,然后锁的过期时间也到了,这时候他自己的锁已经释放,获得锁的已经是其他线程,而活过来的线程继续操作之后 执行他本该执行的删除锁操作,这时候就会删除另一个线程的锁。

答:可以在锁的value保存上每个线程独有的编号,可以使用uuid,这样在删除前先判断一下是不是自己的锁,是的话就删。

3、为防止查询和减库存的这两个操作之间有以外发生,还可以使用Lua脚本让查询和删除同时执行,。

代码实现:此处省略Redis,Jedis集成到Spring的过程。这个网上搜索很多。

这是一个controller类
@Controller
public class SeckillController {
    //
    @RequestMapping("xixi")
    @ResponseBody
    public String ook() {

        //
        service xixi = new service();
        xixi.jian();

        //给前端随便返回一个值,主要是去看Redis中的值得变化。
        return "okokok";
    }
}

 

这是Service,在这里实现分布式锁
@Service
public class service {
    //获取jedis连接
    JedisUtil jedisUtil = new JedisUtil();

    public String jian() {
        System.out.println(Thread.currentThread().getName() + "进入的商品详情的请求");
        // 获得jedis实例
        Jedis jedis = jedisUtil.getJedis();
        // 获得分布式锁中value的值(每个线程独有的,为了解决问题二)
        String token = UUID.randomUUID().toString();
        // 尝试获取分布式锁
        String OK = jedis.set("id:lock", token, "nx", "px", 2 * 1000);// 拿到锁的线程有2秒的过期时间
        //获取锁成功
        if ("OK".equals(OK)) {
            String num = jedis.get("id");
            //判断是否有库存
            //如果没有库存,删除自己的锁
            if (Integer.parseInt(num) < 1) {
                System.out.println("没了");
                jedis.del("id:lock");// 用token确认删除的是自己的sku的锁
                return null;
            }
            System.out.println(Thread.currentThread().getName() + "获得锁");
            //减库存
            jedis.decr("id");
            // 分布锁释放
            System.out.println(Thread.currentThread().getName() + "使用完毕,将锁归还:");
            String lockToken = jedis.get("id:lock");
            if (!lockToken.isEmpty() && lockToken.equals(token)) {
                //jedis.eval("lua");可使用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生
                jedis.del("id:lock");// 用token确认删除的是自己的sku的锁
            }
            jedis.close();
        } else {
            // 获取失败
            System.out.println(Thread.currentThread().getName() + "---------没有拿到锁失败");
            jedis.close();
        }
        return "oooook";
    }

}

解决方案四:

     Redis事务,开启watch监听,执行事务操作。

@Controller
public class SeckillController{

    @RequestMapping("pipi")
    @ResponseBody

    public String okk() {
        service pipi = new service();
        pipi.shiwu();

        return "okokok";
    }
}
@Service
public class service {
    JedisUtil jedisUtil = new JedisUtil();

    public String shiwu() {
        System.out.println(Thread.currentThread().getName() + "进入的商品详情的请求");
        Jedis jedis = jedisUtil.getJedis();

        Integer num = Integer.parseInt(jedis.get("id"));
        if (num > 0 ) {
            //开启事务
            jedis.watch("id");//保证一致性
            Transaction tx = jedis.multi();//开启事务
            tx.incrBy("id", -1);//扣减库存
            List list =tx.exec();//执行事务

            if(list != null){
                System.out.println(Thread.currentThread().getName() + "买了");
            }
            else{
                System.out.println(Thread.currentThread().getName() + "被别人抢了");
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "没货了买失败");

        }
        jedis.close();

        return "xixixixix";
    }
} 
  

总结:

经过我对后两种方法的使用Jmeter的压力测试发现,后两种方法都很稳,在从10~5000的并发中,后两者在测试结果中没有什么太大的差异,用时等都差不多。所以我觉得 解决秒杀,或者类似高并发问题,使用后面两者都可以,至于具体使用哪个,可能得根据自己的需求,或者业务需要来选择。

你可能感兴趣的:(java实用工具程序)