开场
开场
开场之前因为是想到Redis在高并发场景下的应用,所以就拿商品秒杀业务来练练手了,也没有用数据库,用的是Map来模拟数据库表,包括下单后之后库存的扣减,生成订单,都是用Map来模拟。关于Redis的一下应用和知识点,大家可在Redis指南上了解,是中文版的 ,里面包含了一些常用的命令和详细教程,在后文中我们也会主要用到两个常用的命令。我们给大家讲解。高并发之下我是用apache的ab工具来进行压测的,如命令:➜ ~ ab -n 200 -c 100 http://localhost:8080/kill/seckill/123456。(-c 表示并发的用户数量,-n 表示总的请求数量)如果读者没有安装apache可参考本文进行apache压力测试安装。好了到这里就基本讲了一下工具的使用,我们先来看两个需要用到的Redis命令吧。
setnx命令,给一个key设置value值,如果该key之前没有默认值或者设置过默认值,则利用该命令设置成功后返回1,如果已经设置过值,则返回0.
getset命令:自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误(官网上解释的)。如果真的难理解可以把get&set分开,先获取之前key的value,在设置一个新的值给key。
getset命令例子:
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redi
INCR(表示自增)让mycounter的值为1,然后getset命令获取的值为 1,但是又给mycounter设置了一个新的值 0,直接GET命令获取到的就是0;
开始讲解代码:
spring构建的使用SpringBoot构建的,小伙伴们也可以用spring来写,工具是IDEA,不过性质都是一样的,在乎的不是装饰而是过程与结果的正确与否。
我的初衷只是想通过一个简单的例子来让大家对分布式锁有个简单的认识,并不是有多高级的知识,用到的也就几个方法,关键是他的设计思想比较好,关于redis很重要的一点就是它是单线程的。
application.yml 配置文件:spring:
redis:
host: 192.168.1.104 (填写安装redis的主机地址)
port: 6379 (默认端口)如果有密码的话也要填写上密码
redis:
host: 192.168.1.104 (填写安装redis的主机地址)
port: 6379 (默认端口)如果有密码的话也要填写上密码
pass:XXX
pom文件:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>
</dependencies>
Controller:
@Controller
@RequestMapping("/kill")
public class SeckillProductController {
@Autowired
private SeckillProductService seckillProductService;
Map<String,String> map = new HashMap<>();
@GetMapping("/seckill/{productId}")
public ModelAndView Seckill(@PathVariable String productId)
{
String info = seckillProductService.query(productId);
seckillProductService.seckill(productId);
map.put("info",info);
return new ModelAndView("/info",map);
}
}
大家只要想到controller文件就能想到前端页面吧,很简单用了一个freemarker的渲染,首先在pom文件中引出freemarker依赖。
freemarker:
<html>
<h2>${info}</h2>
</html
上面三行代码就好了,用的是EL表达式,通过key值访问info信息。
Service层:
@Component
public class SeckillProductService {
private static final int TIMEOUT = 10 * 1000;
@Autowired
private RedisLock redisLock;
static Map<String,Integer> products = new HashMap<>();
static Map<String,Integer> stocks = new HashMap<>();
static Map<String,Integer> orders =null;
static
{
// 模拟多张表
orders = new HashMap<>();
//商品ID和商品库存
products.put("123456",10000);
stocks.put("123456",10000);
}
/**
* 模拟数据库操作
*/
public String query(String productId){
return "中秋活动价格,月饼限量"+products.get(productId)+
",还剩"+stocks.get(productId)+
",该商品成功下单人数"+orders.size()+"人";
}
/**
* 秒杀业务主方法
* @param productId
*/
public void seckill(String productId)
{
// 当前时间 + 超时时间
// 这个时间不能在定义为类变量 只能定义为局部变量 不然并发就出错了。
long timeOut = System.currentTimeMillis() + TIMEOUT;
// 1. 加锁
if(!redisLock.lock(productId,String.valueOf(timeOut)))
{
throw new RuntimeException("哎哟喂人也太多了,换个姿势再试试");
}
// 根据订单号获取库存
int stockNum = stocks.get(productId);
if(stockNum == 0) // 如果库存为空 抛出异常
{
throw new RuntimeException("库存没了");
}else
{
// 模拟订单入库
orders.put(UUID.randomUUID().toString(),Integer.parseInt(productId));
stockNum = stockNum - 1;// 扣减库存
stocks.put(productId,stockNum);// 模拟更新数据库
}
// 2.解锁
redisLock.unlock(productId,String.valueOf(timeOut));
}
}
redis的加锁解锁方法提出来:
@Component
public class RedisLock {
private Logger logger = LoggerFactory.getLogger(RedisLock.class);
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key 入参 为productID
* @param value unix时间戳格式 当前时间 + 超时时间
* @return Boolean
*/
public boolean lock(String key,String value)
{
// 相当于setnx 如果初次设置key 返回true 1 ,如果是已经设置过 返回false 或者0
// 相当于Redis中的SETNX方法
if(redisTemplate.opsForValue().setIfAbsent(key,value))
{
return true;
}
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)) {
// 删除key值就相当于解锁
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
logger.error("redis分布式锁{}",e);
}
}
}
结果截图:
使用Redis成功后,剩余 + 成功下单人数会一直等于 总的份数,这样才表示成功。
ab压力测试:
总结:
在设计并发业务的时候:第一次想到使用的是synchronized关键字,也能把完成并发业务,但是synchronized无法完成细粒度的操作,因为我们在操作时候只针对一个商品进行高并发业务,只适合单点业务,如果多个商品进行并发操作,每个并发用户都需要操作seckill方法,如果一个商品并发的用户较多,另一个商品并发业务较少,但是同样造成业务的等待,所以使用redis是最好的选择。