一、业务场景
中秋佳节,进行月饼秒杀,特价,限量1000份,不限每人秒的份数,不要超卖即可。
二、SetNX实现
1. 分布式锁SetNX实现
RedisLock.java
/**
* redis 分布式锁
*/
@Component
@Slf4j
public class RedisLock {
@Autowired
private RedisTemplate redisTemplate;
/**
* 加锁
* @param key productId - 商品的唯一标志
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value){
if(redisTemplate.opsForValue().setIfAbsent(key,value)){//对应setnx命令
//可以成功设置,也就是key不存在
return true;
}
//判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentValue = (String) redisTemplate.opsForValue().get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不为空且小于当前时间
//获取上一个锁的时间value
String oldValue = (String)redisTemplate.opsForValue().getAndSet(key,value);//对应getset,如果key存在
//假设两个线程同时进来,key被占用了。获取的值currentValue=A(get取的旧的值肯定是一样的),两个线程的value都是B,key都是K.锁时间已经过期了。
//而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的value已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
//oldValue不为空且oldValue等于currentValue,也就是校验是不是上个对应的商品时间戳,也是防止并发
return true;
}
}
//无锁
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value){
try {
String currentValue = (String)redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value) ){
redisTemplate.opsForValue().getOperations().delete(key);//删除key
}
} catch (Exception e) {
log.error("[Redis分布式锁] 解锁出现异常了,{}",e);
}
}
}
2. 业务模拟实现
1) 业务接口
public interface SeckillService {
/**
* 查询特价商品
* @param productId
* @return
*/
String querySecKillProductInfo(String productId);
/**
* 秒杀的逻辑方法
* @param productId
*/
void orderProductMocckDiffUser(String productId);
}
3) 业务实现
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
@Autowired
private RedisLock redisLock;
private static final int TIMEOUT = 10*1000;//超时时间 10s
/**
* 活动,特价,限量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<>();
//其中666666为单号
products.put("666666",1000);
stock.put("666666",1000);
}
private String queryMap(String productId){//模拟查询数据库
return "中秋活动,月饼特卖,限量"
+products.get(productId)
+"份,还剩:"+stock.get(productId)
+"份,该商品成功下单用户数:"
+orders.size()+"人";
}
@Override
public String querySecKillProductInfo(String productId) {
return this.queryMap(productId);
}
//解决方法二,基于Redis的分布式锁
// http://redis.cn/commands/setnx.html
// http://redis.cn/commands/getset.html
// SETNX命令 将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
// GETSET命令 先查询出原来的值,值不存在就返回nil。然后再设置值
//支持分布式,可以更细粒度的控制
//多台机器上多个线程对一个数据进行操作的互斥。
//Redis是单线程的!!!
@Override
public void orderProductMocckDiffUser(String productId) {
//解决方法一:synchronized锁方法是可以解决的,但是请求会变慢,请求变慢是正常的。
// 主要是没做到细粒度控制。比如有很多商品的秒杀,但是这个把所有商品的秒杀都锁住了。
// 而且这个只适合单机的情况,不适合集群
//加锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"很抱歉,人太多了,换个姿势再试试~~");
}
//1.根所单号666666,查询该商品库存,为0则活动结束
int stockNum = stock.get(productId);
if(stockNum==0){
throw new SellException(100,"活动结束");
}else {
//2.下单 key为生成单号,值为 商品ID也就是 666666
orders.put(KeyUtil.getUniqueKey(),productId);
//3.下单成功,减库存
stockNum =stockNum-1;
// 不做处理的话,高并发下会出现超卖的情况,下单数,
// 大于减库存的情况。虽然这里减了,但由于并发,减的库存还没存到map中去。
// 新的并发拿到的是原来的库存
try{
Thread.sleep(100);//模拟减库存的处理时间
}catch (InterruptedException e){
e.printStackTrace();
}
//4. 商品更新库存
stock.put(productId,stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
}
- 工具类
SellException .java
/**
* 自定义异常
*/
@Data
public class SellException extends RuntimeException{
private Integer code;
public SellException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
public SellException(Integer code, String defaultMessage) {
super(defaultMessage);
this.code=code;
}
}
ResultEnum.java
@Getter
public enum ResultEnum {
//消息枚举
SUCCESS(0,"成功"),
PARAM_ERROR(1,"参数不正确")
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
KeyUtil.java
public class KeyUtil {
/**
* 生成唯一主键
* 格式:时间+随机数
* @return
*/
public static synchronized String getUniqueKey(){//加一个锁
Random random = new Random();
Integer number = random.nextInt(900000) + 100000;//随机六位数
return System.currentTimeMillis()+String.valueOf(number);
}
}
商品查询
秒杀下单
- 并发测试
三、使用INCR实现
@RestController
@Slf4j
@RequestMapping(value = "/secondSkill", produces = "application/json;charset=UTF-8")
public class SecondSkillController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/initData")
public String initData() {
redisTemplate.opsForValue().set("stock", 100);
redisTemplate.opsForValue().set("count", 0);
return "商品库存数量为:" + redisTemplate.opsForValue().get("stock") + "抢到商品人数为:"
+ redisTemplate.opsForValue().get("count");
}
@GetMapping("/secondSkill")
public String secondSkill() {
String stock = (String) redisTemplate.opsForValue().get("stock");
String count = (String) redisTemplate.opsForValue().get("count");
int pCount = Integer.valueOf(count);
int stockNum = Integer.valueOf(stock);
// Long lock = redisTemplate.opsForValue().increment("lock", 1);
//应为1
// if (lock == 1) {
// redisTemplate.expire("lock",10L, TimeUnit.SECONDS);
if (stockNum > 0) {
try {
pCount++;
redisTemplate.opsForValue().set("count", pCount);
log.info("库存数量为" + stockNum);
//业务逻辑
stockNum--;
//更改库存
redisTemplate.opsForValue().set("stock", stockNum);
} catch (Exception e) {
//解锁
redisTemplate.opsForValue().increment("lock", -1);
}
} else {
return "库存不足";
}
// }
return "商品库存剩余数量为:" + stockNum + "-》" + redisTemplate.opsForValue().get("count") + "人成功购买1件商品";
}
@GetMapping(value = "/queryStock")
public String queryStock() {
return "剩余商品库存数量为:" + redisTemplate.opsForValue().get("stock") + "-》" + redisTemplate.opsForValue().get("count") + "人成功购买1件商品";
}
}
-
测试
初始化数据:
http://localhost:8081/secondSkill/queryStock/initData
查看库存:
http://localhost:8081/secondSkill/queryStock
秒杀地址:
http://localhost:8081/secondSkill/secondSkill -
并发测试结果
1)没有加锁
2)无解锁
3)解锁
四、互斥锁存在的问题
- 如果业务中出现问题,出异常不没有执行到解锁。
解决方案:
在finally中添加解锁操作。 - 如果在集群中有一台机子抢到锁,但宕机了。
解决方案:
添加过期时间。 - 如果有一个业务操作耗时会超过设定的过期时间。
需要使用后台线程进监控操作,不断的去监控锁,如果该线程还存在,就给锁续期。也就是该锁必须有该线程进行解锁。