Redis应用(4)分布式锁(4.1)------并发问题与分布式锁引入

目录

一、背景demo:

二、解决方法:

三、分布式锁:

四、Redis锁:


一、背景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<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);

二、并发加锁:

为达到这样的效果:

      

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条记录。

三、分布式锁:

1、问题总结:

(1)以上是单节点下的高并发问题;部署多个节点,单个节点存在并发问题,多个节点自然也存在并发问题;即使单个节点不存在并发问题,某些场景下,多节点访问同一个数据库也存在并发问题。如定时job,两个节点同时去读写数据库,任务可能重复执行。

(2)多节点下的并发问题:

在集群下,或者在分布式系统下,每个线程有自己的jvm,多个jvm存在,每个jvm都有自己的锁监视器,会有多个线程获得锁,可能出现安全问题,所以我们要想办法,让多个jvm使用同一把锁 。

关键是让多个进程看到同一个锁,并且只有一个线程能拿到锁。 

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表示秒

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w_t_y_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值