-
Notifications
You must be signed in to change notification settings - Fork 626
测压工具的使用和redis分布式锁
sqmax edited this page Jul 8, 2018
·
8 revisions
分布式锁都用在高并发、大流量的场景,之前的Postman模拟用户下单的操作,无法模拟这种情景,所以这里用压测工具来模拟并发。
- 测压工具:Apache ab
- 命令:
ab -n 100 -c 100 http://www.XXXX.com
这里-n表示发出100个请求,-c模拟100个线程并发,相当于100个人同时访问,最后是我们要测试的url; 也可以这样写:
ab -t 60 -c 100 http://www.XXX.com/
-t表示60秒,-c表示100个并发,它会在连续60秒内不停地发请求。
更详细的命令说明,见官方文档 - 命令:
Apache ab安装方法: 这里有一篇详细的安装方法:https://blog.csdn.net/ahaaaaa/article/details/51514175
注意:
- 最好解压到磁盘根目录下,我开始解压到Download目录下,运行:
httpd.exe
命令就出错了。 - 解压后的根目录下有个说明文档
readme_first.html
,里面有介绍方式。
下面开始做实验:
- 模拟秒杀的业务方法如下:
@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);
}
//解锁
}
Controller方法如下:
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId)throws Exception
{
log.info("@skill request, productId:" + productId);
secKillService.orderProductMockDiffUser(productId);
return secKillService.querySecKillProductInfo(productId);
}
2 用压测工具模拟秒杀
ab -n 1000 -c 500 http://127.0.0.1:8080/sell/skill/order/123456
查看结果:http://127.0.0.1:8080/sell/skill/query/123456
国庆活动,皮蛋粥特价,限量份100000 还剩:99992 份 该商品成功下单用户数目:1000 人
出现超卖现象。
-
解决方法
方法1: 在秒杀的业务方法上加synchronized
关键字,结果对上了,但是请求慢的像蜗牛。- 缺点:无法做到细粒度控制。当有许多商品时,每个商品id不同,但这里对每个商品的抢购都会加锁,假如秒杀A商品的人很多,秒杀B商品的人很少,一旦进入这个方法都会造成一样的慢,这就是说无法做到细粒度的控制。
- 只适合单点的情况。只能跑在单机上。
方法2:解决思路,保证下面一段代码单线程访问。
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);
}
我们要在这段代码之前和之后分别加锁和解锁。
- 加锁逻辑(很难理解,多看视频,有非常细致的讲解)
/**
* 加锁
* @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) && currentValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e) {
log.error("【redis分布式锁】解锁异常, {}", e);
}
}
最终秒杀的业务方法如下:
@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));
}
优点:
- 支持分布式。
- 可以更细粒度的控制(秒杀同一种商品才会加锁,而不同的商品可以同时访问这个方法,不会同步)。
redis官网,查看命令含义try redis。
上面主要要到:setnx
,getset
命令。
秒杀实验中,redis分布式锁解决方案的完整代码。
package com.imooc.controller;
import com.imooc.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
*/
@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);
}
}
package com.imooc.service.impl;
import com.imooc.exception.SellException;
import com.imooc.service.RedisLock;
import com.imooc.service.SecKillService;
import com.imooc.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,"哎呀,人太多了,换个姿势再来!");
}
//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));
}
}
package com.imooc.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@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;
}
/**
* 解锁
* @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)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e) {
log.error("【redis分布式锁】解锁异常, {}", e);
}
}
}
目录