模拟秒杀代码
import com.wechatorder.sell.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/skill")
@Slf4j
public class SeckillController {
@Autowired
private SeckillService seckillService;
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
* @throws Exception
*/
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId) throws Exception{
return seckillService.querySeckillProductInfo(productId);
}
/**
* 秒杀 抢到了会返回剩余库存量
* @param productId
* @return
* @throws Exception
*/
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId) throws Exception{
log.info("@skill request,productId"+productId);
seckillService.orderProductMockDiffUser(productId);
return seckillService.querySeckillProductInfo(productId);
}
}
public interface SeckillService {
String querySeckillProductInfo(String productId);
void orderProductMockDiffUser(String productId);
}
@Service
public class SeckillServiceImpl implements SeckillService {
/**
* 活动皮蛋粥特价 限量100000份
*/
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static {
/**
* 模拟多个表,商品信息表,库存表,秒杀成功订单表
*/
products=new HashMap<>();
stock=new HashMap<>();
orders=new HashMap<>();
products.put("123456",100000);
stock.put("123456",100000);
}
private String queryMap(String productId){
return "国庆活动,皮蛋粥特价,限量份"
+products.get(productId)
+"还剩:"+stock.get(productId)+"份"
+"该商品成功下单用户数目:"
+orders.size()+"人";
}
@Override
public String querySeckillProductInfo(String productId) {
return this.queryMap(productId);
}
@Override
public void orderProductMockDiffUser(String productId) {
//查询该商品库存,为0则活动结束.
int stockNum=stock.get(productId);
if (stockNum==0){
throw new SellException(100,"活动结束");
}else {
//下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//减库存
stockNum=stockNum-1;
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
}
这里减库存的代码如果在高并发环境下会出现问题
使用Apache ab压测模拟并发
ab -n 100 -c 10 http://127.0.0.1:8080/sell/skill/order/123456
-n 50 表示表示发出100个请求
-c 10 表示10个并发数
这时发现并没有出现问题
然后执行
ab -n 500 -c 100 http://127.0.0.1:8080/sell/skill/order/123456
这时就产生了超卖的现象
很多学过多线程的小伙伴肯定会说使用synchronized关键字加锁就可以解决了
代码如下
@Override
public synchronized void orderProductMockDiffUser(String productId) {
//查询该商品库存,为0则活动结束.
int stockNum=stock.get(productId);
if (stockNum==0){
throw new SellException(100,"活动结束");
}else {
//下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//减库存
stockNum=stockNum-1;
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
然后我们再次执行
ab -n 500 -c 100 http://127.0.0.1:8080/sell/skill/order/123456
这次没有出现问题 但是速度明显的慢了很多
synchronized的确是一种解决办法,但是无法做到细粒度的控制,而且只适合单点的情况
我们刚刚是把他加到一个方法上,假如我们有很多个商品,每个商品的id不一样 但是他们都要访问这个方法,假如秒杀A商品的人很多,秒杀B商品的人很少,一旦进入这个方法都会造成一样的慢.
那有没有其他的办法呢
可以使用基于redis的分布式锁来解决
我们先来了解两个redis指令
SETNX key value
**时间复杂度:**O(1)
将key
设置值为value
,如果key
不存在,这种情况下等同SET命令。 当key
存在时,什么也不做。SETNX
是”SET if Not eXists”的简写。
返回值
Integer reply, 特定值:
1
如果key被设置了0
如果key没有被设置
例子
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis>
GETSET key value
**时间复杂度:**O(1)
自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。
返回值
bulk-string-reply: 返回之前的旧值,如果之前Key
不存在将返回nil
。
例子
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redis>
代码实现
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.simpleframework.xml.core.Commit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value){
if (redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
//currentValue=A 这两个线程的value都是B 只会让其中一个线程拿到锁
String currentValue =redisTemplate.opsForValue().get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue)&&Long.parseLong(currentValue)<System.currentTimeMillis()){
//获取上一个锁的时间
String oldValue=redisTemplate.opsForValue().getAndSet(key,value);
if (!StringUtils.isEmpty(oldValue)&&oldValue.equals(currentValue)){
return true;
}
}
return false;
}
public void unlock(String key,String value){
try {
String currentValue =redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue)&¤tValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
log.error("redis分布式锁 解锁异常,{}",e);
}
}
}
import com.wechatorder.sell.exception.SellException;
import com.wechatorder.sell.service.RedisLock;
import com.wechatorder.sell.service.SeckillService;
import com.wechatorder.sell.utils.KeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class SeckillServiceImpl implements SeckillService {
private static final int TIMEOUT=10*1000;//超时时间10s
@Autowired
private RedisLock redisLock;
/**
* 活动皮蛋粥特价 限量100000份
*/
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static {
/**
* 模拟多个表,商品信息表,库存表,秒杀成功订单表
*/
products=new HashMap<>();
stock=new HashMap<>();
orders=new HashMap<>();
products.put("123456",100000);
stock.put("123456",100000);
}
private String queryMap(String productId){
return "国庆活动,皮蛋粥特价,限量份"
+products.get(productId)
+"还剩:"+stock.get(productId)+"份"
+"该商品成功下单用户数目:"
+orders.size()+"人";
}
@Override
public String querySeckillProductInfo(String productId) {
return this.queryMap(productId);
}
@Override
public void orderProductMockDiffUser(String productId) {
//加锁
long time=System.currentTimeMillis()+TIMEOUT;
if (!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"人也太多了,换个姿势再试试~");
}
//查询该商品库存,为0则活动结束.
int stockNum=stock.get(productId);
if (stockNum==0){
throw new SellException(100,"活动结束");
}else {
//下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//减库存
stockNum=stockNum-1;
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
}
这时我们再来测试一次
显示只有5个人 但是总数是对的上的,说明只有5人拿到了锁
redis实现分布式锁的优点:
支持分布式
可以更细粒度的控制
分布式锁可以概括为:
多台机器上多个进程对一个数据进行操作的互斥
我们利用了setnx和getset命令这种特点实现了分布式锁.redis适合做分布式锁一个很重要的原因其实是因为redis是单线程的