使用springboot+spring data jpa +redis实现一个简单的后台秒杀程序:
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
controller层:
@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);
}
/**
* 秒杀,没有抢到获得"被挤爆了,xxxx",抢到了会返回剩余的库存量
* @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);
}
}
service层:
public interface SecKillService {
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
String querySecKillProductInfo(String productId);
/**
* 模拟不同用户秒杀同一商品的请求
* @param productId
* @return
*/
void orderProductMockDiffUser(String productId);
}
这其实就是多线程的具体场景,那么最常见的方法是在 下单的方法上orderProductMockDiffUser加上synchronized关键字,但是这就会有缺陷:1、只能实现单机版应用,2、程序太慢。
所以考虑使用redis:
@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.getUniqueKey(),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加锁和解锁:
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate template;
/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value){
//使用redis的setnx命令,这里如果设置成功,返回true说明已经被锁住
/**
* 如果被锁,那么新值设置不进去
*/
if(template.opsForValue().setIfAbsent(key,value)){
return true;
}
/**
* 下面这段代码如果不加,可能发生死锁的情况
*/
//currentValue=A 这两个线程的value都是B 其中一个线程拿到锁
String currentValue = template.opsForValue().get(key);
//如果锁过期 currentValue =(当前时间+过期时间)
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){
//使用getset方法,获取上一个锁的时间
String oldValue = template.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 = template.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
template.opsForValue().getOperations().delete(key);
}
}catch(Exception e){
// log.error("【redis分布式锁】解锁异常, {}", e);
}
}
}
redis中文网:http://www.redis.cn/