一、背景demo:
1、代码:
product商品表:
userorder订单表:
现有抢购活动:
@RequestMapping("/product")
@RestController
public class ProductController {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
//抢购
@RequestMapping("/order")
public void order(@RequestParam String userId,@RequestParam String productId){
orderService.order(productId,userId);
}
//库存
@RequestMapping("/select")
public Integer select(@RequestParam String productId){
Integer count = productService.getCountByProductId(productId);
return count;
}
}
service实现,主要是订单service:
@Service("orderService")
public class OrderServiceImpl implements OrderService{
@Autowired
private ProductService productService;
@Autowired
private UserOrderMapper userOrderMapper;
//用户下订单,返回订单id
@Override
public Integer order(String productId, String userId) {
//先判断productId是否存在
Product product = productService.getByProductId(productId);
if(product == null){
return null;
}
//是否有库存
Integer id = product.getId();
Integer total = product.getTotal();
System.out.println("下单前库存"+total);
if(total <= 0){
return null;
}
UserOrder order = new UserOrder();
order.setCreatetime(new Date());
order.setProductid(productId);
order.setUserid(userId);
int add = userOrderMapper.addOrder(order);
if(add > 0){
//创建订单成功,库存--
total--;
System.out.println("下单后库存"+total);
productService.updateTotal(id,total);
}
return order.getId();
}
@Override
public Integer getCountByProductId(String productId) {
return userOrderMapper.getCountByProductId(productId);
}
}
启动这个工程,运行在本地8080端口。
2、测试:先在数据库中预设有productid为abcd的商品,库存为5。再另建一个工程模拟多线程并发:
public class HttpRequestUtil {
/**
* 向指定URL发送GET方法的请求
*
* @param url
* 发送请求的URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return URL 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map> map = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url
* 发送请求的 URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, String param) {
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
out.print(param);
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送 POST 请求出现异常!"+e);
e.printStackTrace();
}
//使用finally块来关闭输出流、输入流
finally{
try{
if(out!=null){
out.close();
}
if(in!=null){
in.close();
}
}
catch(IOException ex){
ex.printStackTrace();
}
}
return result;
}
}
package com.test.test;
import com.test.util.HttpRequestUtil;
public class ProductTest implements Runnable{
public static void main(String[] args) {
ProductTest productTest = new ProductTest();
for(int i=0;i<50;i++){
Thread thread = new Thread(productTest);
thread.start();
}
// String url = "http://localhost:8080/product/select";
// String productId = "abcd";
// String param = "productId="+productId;
// String count = HttpRequestUtil.sendPost(url, param);
// System.out.println("总订单数"+count);
}
@Override
public void run() {
String url = "http://localhost:8080/product/order";
String productId = "abcd";
String userId = "userid";
String param = "userId="+userId+"&productId="+productId;
HttpRequestUtil.sendPost(url, param);
}
}
随机一次,看下数据库的变化:
在并发量为50的情况下,商品库存是变为0了,
但实际下单的订单数却不只5次。 原因是多个用户进入到下单逻辑代码后,同时执行了int add = userOrderMapper.addOrder(order);
二、分布式锁:锁一般考虑分布式锁,解决分布式部署架构中的并发问题。
(1)以上是单节点下的高并发问题;
(2)多节点下的并发问题:部署多个节点,单个节点存在并发问题,多个节点自然也存在并发问题;即使单个节点不存在并发问题,某些场景下,多节点访问同一个数据库也存在并发问题。如定时job,两个节点同时去读写数据库,任务可能重复执行。
三、分布式锁的实现方式:分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:
四、redis锁解决demo中的问题:为达到这样的效果:
1、方法1:使用synchronized修饰:
修饰方法使之成为同步方法:
@Override
public synchronized Integer order(String productId, String userId) {
// 先判断productId是否存在
Product product = productService.getByProductId(productId);
if (product == null) {
return null;
}
// 是否有库存
Integer id = product.getId();
Integer total = product.getTotal();
System.out.println("下单前库存" + total);
UserOrder order = new UserOrder();
if (total <= 0) {
return null;
}
order.setCreatetime(new Date());
order.setProductid(productId);
order.setUserid(userId);
int add = userOrderMapper.addOrder(order);
if (add > 0) {
// 创建订单成功,库存--
total--;
System.out.println("下单后库存" + total);
productService.updateTotal(id, total);
return order.getId();
}
return null;
}
2、方法二:使用redis:原理,redis的SETNX 操作set only if not exist。
@RequestMapping("/product")
@RestController
public class ProductController {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@Autowired
private RedisUtil redisUtil;
//private String GROUP_LOCK = "produce:lock:{productId}";
private String KEY = "productId";
@RequestMapping("/order")
public void order(@RequestParam String userId,
@RequestParam String productId) {
boolean lock = redisUtil.exists(KEY);
if (!lock) {
//
Long result = redisUtil.addValue(KEY, productId);
redisUtil.disableTime(KEY, 60);
if (result == 1) {
System.err.println("不存在key,执行逻辑操作");
orderService.order(productId, userId);
redisUtil.delKey(KEY);
}
}else{
System.out.println("存在该key,不允许执行");
}
}
@RequestMapping("/select")
public Integer select(@RequestParam String productId) {
Integer count = productService.getCountByProductId(productId);
return count;
}
}
其中:
/**
* @Description:redis工具类
*/
@SuppressWarnings("unused")
@Component
public class RedisUtil {
private static final String IP = "127.0.0.1"; // ip
private static final int PORT = 6379; // 端口
// private static final String AUTH=""; // 密码(原始默认是没有密码)
private static int MAX_ACTIVE = 1024; // 最大连接数
private static int MAX_IDLE = 200; // 设置最大空闲数
private static int MAX_WAIT = 10000; // 最大连接时间
private static int TIMEOUT = 10000; // 超时时间
private static boolean BORROW = true; // 在borrow一个事例时是否提前进行validate操作
private static JedisPool pool = null;
private static Logger logger = Logger.getLogger(RedisUtil.class);
/**
* 初始化线程池
*/
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(BORROW);
pool = new JedisPool(config, IP, PORT, TIMEOUT);
}
/**
* 获取连接
*/
public static synchronized Jedis getJedis() {
try {
if (pool != null) {
return pool.getResource();
} else {
return null;
}
} catch (Exception e) {
logger.info("连接池连接异常");
return null;
}
}
// 加锁
public boolean tryLock(String key){
Jedis jedis = null;
jedis = getJedis();
String result = jedis.setex(key,1000,"1");
if("OK".equals(result)){
return true;
}
return false;
}
// 释放锁
public void releaseLock(String key){
Jedis jedis = null;
jedis = getJedis();
jedis.del(key);
}
/**
* @Description:设置失效时间
* @param @param key
* @param @param seconds
* @param @return
* @return boolean 返回类型
*/
public static void disableTime(String key, int seconds) {
Jedis jedis = null;
try {
jedis = getJedis();
jedis.expire(key, seconds);
} catch (Exception e) {
logger.debug("设置失效失败.");
} finally {
getColse(jedis);
}
}
public static boolean exists(String key) {
boolean flag = false;
Jedis jedis = null;
try {
jedis = getJedis();
flag = jedis.exists(key);
} catch (Exception e) {
logger.debug("设置失效失败.");
} finally {
getColse(jedis);
}
return flag;
}
/**
* @Description:插入对象
* @param @param key
* @param @param obj
* @param @return
* @return boolean 返回类型
*/
public static boolean addObject(String key, Object obj) {
Jedis jedis = null;
String value = JSONObject.toJSONString(obj);
try {
jedis = getJedis();
jedis.set(key, value);
return true;
} catch (Exception e) {
logger.debug("插入数据有异常.");
return false;
} finally {
getColse(jedis);
}
}
/**
* @Description:存储key~value
* @param @param key
* @param @param value
* @return void 返回类型
*/
public static Long addValue(String key, String value) {
Jedis jedis = null;
try {
jedis = getJedis();
//String code = jedis.set(key, value);
return jedis.setnx(key, value);
} catch (Exception e) {
logger.debug("插入数据有异常.");
return null;
} finally {
getColse(jedis);
}
}
/**
* @Description:删除key
* @param @param key
* @param @return
* @return boolean 返回类型
*/
public static boolean delKey(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
Long code = jedis.del(key);
if (code > 1) {
return true;
}
} catch (Exception e) {
logger.debug("删除key异常.");
return false;
} finally {
getColse(jedis);
}
return false;
}
/**
* @Description: 关闭连接
* @param @param jedis
* @return void 返回类型
*/
public static void getColse(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
项目目录结构:
现在是这样子的:
随机测试,并发量越大时,就是数据库中库存为0 ,订单表有5条记录。