@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId)throws Exception
{
return secKillService.querySecKillProductInfo(productId);
}
/**
* 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
* @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);
}
}
去到SecKillServiceImpl里面的orderProductMockDiffUser看下怎么写的
这里用到3个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);
}
// TODO: redis分布式解锁加锁
/**主要秒杀的逻辑方法**/
@Override
public void orderProductMockDiffUser(String productId)
{
//1.首先查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
}
通过压测工具压测下
压测500个,线程用100个
压测结束,就发现问题
剩余数加上下单数目,不等于总数量,超过10万份
很多请求来的时候,大家都在下单
在sleep过程中,会有很多用户在下单,这就会造成下单数量,大于减库存数量,会多出很多
可能可以说,这里是从map里面查的,所以会出这个问题,如果直接从数据库里面查,select语句加上for update这样就能锁住,就不会出现这个问题。
但是很多东西不放在数据库,就是由于内存,redis这些查询比数据库快很多
假如我们现在需求就是要放在内存中,不能放在数据库。
可以加上synchronized关键字。
/**主要秒杀的逻辑方法**/
@Override
public synchronized void orderProductMockDiffUser(String productId)
{
//1.首先查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
重启,在压测
发现压测越来越慢
synchronized关键字,就是用一个方法将它锁住了,而每次访问这个方法的线程,只会有一个线程,所以就是导致它慢的原因
通过这种方法在保证,这个里面的方法是单线程来处理,不会出现什么问题
synchronized关键字总结
解决死锁和在多个线程同时访问进来的时候,只会让一个线程拿到锁
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key, String value) {
//用到setnx命令,java里面不一样setIfAbsent方法,返回Boolean
//如果setIfAbsent就是被锁定了
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
/**
* 如果没有下面的步骤,就会直接返回false,
* 会直接造成死锁的情况,
* 因为设置了过期时间,
* 就是里面的值小于当前时间的话,进到下面的代码中,就会返回ture
* 返回true,就会解开了那个死锁,即可继续进行下去,而不会一直被锁住
* 还解决一个问题
* 就是在多个线程同时访问进来的时候,只会让一个线程拿到锁
* **/
//如果锁超时,就判断一下,当前的值从redis里面获取key的值
//currentValue=A 这两个线程的value都是B 只会其中一个线程拿到锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
//多个线程进来的时候,只会一个线程拿到锁
//就是存储进去的时间小于当前时间
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
//获取上一个锁的时间
//使用getAndSet方法,此时value值是B
//第一个线程拿到oldvalue值就是A,当前的currentValue也是A,如果相等就返回ture
// 第二个线程拿到oldvalue值是B,但是currentValue一直是A,不相等,就没有拿到这把锁
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
//如果这个值不为空,并且和当前的value值相等的话
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key, String value) {
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
//解锁就是删掉key
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e) {
log.error("【redis分布式锁】解锁异常, {}", e);
}
}
}
// TODO: redis分布式解锁加锁
/**主要秒杀的逻辑方法**/
@Override
public void orderProductMockDiffUser(String productId)
{
//加锁
long time = System.currentTimeMillis() + TIMEOUT;
//返回是布尔类型,判断是否加锁成功
if (!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"哎呦喂,人也太多了,换个姿势再进来试试。。。");
}
//保证下面代码单线程访问
//1.首先查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}