秒杀功能最重要的就是对库存的把控性,所以也就是说一定要让查询修改库存这两个步骤具有原子性。
所以根据我的理解,可以得出以下几种解决方案:
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
经过我对后两种方法的使用Jmeter的压力测试发现,后两种方法都很稳,在从10~5000的并发中,后两者在测试结果中没有什么太大的差异,用时等都差不多。所以我觉得 解决秒杀,或者类似高并发问题,使用后面两者都可以,至于具体使用哪个,可能得根据自己的需求,或者业务需要来选择。