代码:https://codechina.csdn.net/wwwzhouzy/rediswatch
1、将商品数量初始化到redis
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.zhouzy.boot.zhouzyBoot.controller.ApiController;
@Component
@SuppressWarnings("rawtypes")
public class InitData {
Integer shopNum = 110;
private static Logger logger = LoggerFactory.getLogger(InitData.class);
@Autowired
RedisTemplate redisTemplate;
@PostConstruct
public void initRedisShopNum(){
redisTemplate.opsForValue().set("shopNum", shopNum,2,TimeUnit.MINUTES);
logger.info("商品的库存数量为:{}",redisTemplate.opsForValue().get("shopNum"));
}
}
2、接口实现-不加锁的情况
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
import com.zhouzy.boot.zhouzyBoot.model.User;
@RestController
public class ApiController {
private static Logger logger = LoggerFactory.getLogger(ApiController.class);
@Autowired
RedisTemplate redisTemplate;
@SuppressWarnings("unchecked")
@RequestMapping(value = "/api/miaosha",method =RequestMethod.POST)
public String miaosha(@RequestBody User user,HttpServletRequest request){
int shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
logger.info("当前商品数量:{}",shopNum);
if(shopNum <= 0){
logger.info("商品数量不够了,还有:{}",redisTemplate.opsForValue().get("shopNum"));
return "fail";
}
//每个用户只能抢一次,
String userId = user.getUserId();
Integer num = redisTemplate.opsForValue().get(userId) == null ? 0 : Integer.valueOf(redisTemplate.opsForValue().get(userId).toString());
if(num > 0){
logger.info("用户:{},已经抢过了,当前数量:{}",userId,num);
return "fail";
}
num = num + 1;
shopNum = shopNum - 1;
//这个是没有考虑并发的问题场景,可以试试看结果如何
redisTemplate.opsForValue().set(userId, num,2,TimeUnit.MINUTES);
redisTemplate.opsForValue().set("shopNum", shopNum,2,TimeUnit.MINUTES);
logger.info("用户:{},抢成功了,当前数量:{}",userId,num);
shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
//库存数量
logger.info("商品数量还有:{}",shopNum);
return "success";
}
}
3、模拟高并发测试
利用CyclicBarrier 栅栏,100个请求同时调用
package com.zhouzy.boot.zhouzyBoot;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Test;
public class MiaoshaTest {
//JUC工具类 栅栏,实现满一定数量线程后往下执行,跟countdownlatch不同的是,这个可以重复使用
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(100);
@Test
public void test() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0;i<200;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
final int num = i%50;//这样是为了让一批线程里有两个相同的用户去竞争,主要是为了测试并发
executorService.execute(()->{
try {
play("userId"+num,"name"+num);
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
public static void play(String userId,String name) throws BrokenBarrierException, InterruptedException {
System.out.println(Thread.currentThread().getName() + " 已准备");
cyclicBarrier.await();
Map<String,String> map = new HashMap();
map.put("userId", userId);
map.put("name", name);
HttpClientUtil.doPost("http://127.0.0.1:8081/api/miaosha", map, "UTF-8");
System.out.println(Thread.currentThread().getName() + " 开始执行");
}
}
4、测试结果
商品数量竟然未减少
5、使用redis watch功能
package com.zhouzy.boot.zhouzyBoot.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
import com.zhouzy.boot.zhouzyBoot.model.User;
@RestController
public class ApiController {
private static Logger logger = LoggerFactory.getLogger(ApiController.class);
@Autowired
RedisTemplate redisTemplate;
@SuppressWarnings("unchecked")
@RequestMapping(value = "/api/miaosha",method =RequestMethod.POST)
public String miaosha(@RequestBody User user,HttpServletRequest request) throws Exception{
//每个用户只能抢一次,
String userId = user.getUserId();
int shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
logger.info("当前商品数量:{}",shopNum);
if(Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString()) <= 0){
logger.info("商品数量不够了,还有:{}",redisTemplate.opsForValue().get("shopNum"));
return "fail";
}
Integer num = redisTemplate.opsForValue().get(userId) == null ? 0 : Integer.valueOf(redisTemplate.opsForValue().get(userId).toString());
if(num > 0){
logger.info("用户:{},已经抢过了,当前数量:{}",userId,num);
return null;
}
// 开启事务支持,在同一个 Connection 中执行命令
redisTemplate.execute((RedisOperations operations) -> {
//特别注意,在事务方法块里都用operations操作数据,而不是用redisTemplate
//这个是没有考虑并发的问题场景,可以试试看结果如何
List<Object> result = null;
do{
operations.watch("shopNum");
int shopNum2 = Integer.valueOf(String.valueOf(operations.opsForValue().get("shopNum")));
int num2 = operations.opsForValue().get(userId) == null ? 0 : Integer.valueOf(operations.opsForValue().get(userId).toString());
if(num2 > 0){
logger.info("用户:{},已经抢过了,当前数量:{}",userId,num2);
break;
}
//shopNum = shopNum - 1;
operations.multi();//开启事务
operations.opsForValue().set(userId, num2+1);
operations.opsForValue().set("shopNum", shopNum2-1);
result = operations.exec();//执行
logger.info(JSONObject.toJSONString(result));
try {
Thread.sleep(RandomUtils.nextInt(1, 100));
} catch (Exception e) {
e.printStackTrace();
}
}while (CollectionUtils.isEmpty(result)); //如果失败则重试,注意不是null,而是判断非空,包括长度为0
return result;
});
shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
logger.info("执行后当前商品数量:{}",shopNum);
return "success";
}
}
利用redis的watch功能配合事务,会存在一次不成功多次重试的问题,这是乐观锁不能避免的,代码里需要注意的地方都添加了注释,执行结果如下
从执行结果看,充分提现出了redis乐观锁的特征,50个用户200次并发请求, 每个用户一个,最终库存60个数据正常!