目录
一、背景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端口。
测试:先在数据库中预设有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<String, List<String>> 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;
}
}
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);
2、加锁解决
为达到这样的效果:
方法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;
}
方法二:使用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条记录。
二、分布式锁:
1、问题总结
(1)以上是单节点下的高并发问题;部署多个节点,单个节点存在并发问题,多个节点自然也存在并发问题;即使单个节点不存在并发问题,某些场景下,多节点访问同一个数据库也存在并发问题。如定时job,两个节点同时去读写数据库,任务可能重复执行。
(2)多节点下的并发问题:
在集群下,或者在分布式系统下,每个线程有自己的jvm,多个jvm存在,每个jvm都有自己的锁监视器,会有多个线程获得锁,可能出现安全问题,所以我们要想办法,让多个jvm使用同一把锁 。
关键是让多个进程看到同一个锁,并且只有一个线程能拿到锁。 所以synchronized并不能解决多节点下的并发问题。
2、分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁称为分布式锁。
分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:最常用的还是Redis分布式锁
三、Redis做分布式锁:
1、思路:利用set nx ex获取锁,并设置过期时间,保存线程标识;释放锁时先判断线程标识是否与自己一致,一致则删除
2、特性:利用set nx满足互斥性;利用set ex保证故障时锁依然能释放,避免死锁,提高安全性;利用Redis集群保证高可用和高并发特性
3、redis实现加锁的几种命令:redis能用的的加锁命令分表是INCR、SETNX、SET
(1)INCR:这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。
1、 客户端A请求服务器获取key的值为1表示获取了锁
2、 客户端B也去请求服务器获取key的值为2表示获取锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
5、 客户端B执行代码完成,删除锁
$redis->incr($key);
$redis->expire($key, $ttl); //设置生成时间为1秒
(2)SETNX:这种加锁的思路是,如果 key 不存在,将 key 设置为 value;如果 key 已存在,则 SETNX 不做任何动作
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
(3)SET:上面两种方法都需要设置 key 过期,这是防止意外情况锁无法释放。但是借助 Expire 来设置就不是原子性操作了,所以官方就引用了另外一个,使用 SET 命令本身已经从版本 2.6.12 开始包含了设置过期时间的功能。
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁
$redis->set($key, $value, array('nx', 'ex' => $ttl)); //ex表示秒
四、redis分布式锁的思路
1、介绍:
分布式锁既不是给同一个进程中的多个线程使用,也不是给同一台机器上的多个进程使用,而是由不同机器上的不同Redis客户端进行获取和释放的。
RedLock是Redis之父Salvatore Sanfilippo提出来的基于多个Redis实例的分布式锁的实现方案。其核心思想就在于使用多个Redis冗余实例来避免单Redis实例的不可靠性。RedLock采用的就是依据法团准则的方案:
2、redis分布式锁的过程分析:
redis分布式锁就几个方法:
① setnx(key,value) 返回boolean 1为获取锁 0为没获取锁
② expire() 设置锁的有效时间
③ getSet(key,value) 获取锁当前key对应的锁的有效时间
④ deleteKey() 删除锁
(1)setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2;
(2)get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3;
(3)计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
(4)在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
3、可靠性要求:
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
(1)互斥性。在任意时刻,只有一个客户端能持有锁。
(2)不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
(3)具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
(4)加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。