Redis实现分布式锁

本文探讨了高并发环境下秒杀活动的技术挑战,通过分析多线程并发处理时产生的“超卖”现象,介绍了使用synchronized和Redis分布式锁两种解决方案。详细解释了Redis分布式锁的实现原理,包括setnx和getset命令的应用,以及如何避免死锁问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题:

对于高并发的数据请求(简称秒杀活动~~),比如像双十一的活动,有很多的用户去买东西,(哦,偶的荷包。。)

额,,对于如此之大的数据量,我都怀疑在多线程的处理下,这,数据不丢失,我当时测试多线程的时候如果线程过多的话,1+1都可能不去等于2了,而这样的情况就会产生所谓的“超卖”现象。。

所以,我有点吃多了,非要去弄一下这个高并发线程处理数据的问题。。

果然,我被gank了,额,这里因为重做电脑,测试软件没了,截图也没了。。

反正知道多线程的人都会知道,,我模拟了一下“秒杀”事件

这里来一段代码(模拟“秒杀商品”情况)

public class KillServiceImpl implements KillService {

	private static final int TIMEOUT = 10 * 1000;//超时 10s
	@Autowired
	private RedisLock redisLock;
	/**
	 * 某产品,数量为10w
	 */
	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("1", 100000);
		stock.put("1", 100000);
	}

	private String queryMap(String productId) {
		System.out.println("商品" + products.get(productId) + "还剩: " + stock.get(productId) + "份" + "成功下单人数: "
				+ orders.size() + "人");
		return null;
	}

	@Override
	public String query(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 RuntimeException();
		}
		int stockNum = stock.get(productId);
		// 查询该商品的库存,若为0,则终止,即秒杀活动 结束
		if (stockNum == 0) {
			throw new RuntimeException();

		} else {
			// 下单
			orders.put(genUniqueKey(), productId);
			// 减库存
			stockNum = stockNum - 1;
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			stock.put(productId, stockNum);
		}
		//解锁
		redisLock.unLock(productId, String.valueOf(time));
	}

	public static synchronized String genUniqueKey() {
		Random random = new Random();
		Integer number = random.nextInt(900000) + 100000;
		return System.currentTimeMillis() + String.valueOf(number);
	}

}

这就是模拟的 sever层的代码,因为是在一个web项目上测试的,所以,你懂的,Controller层什么的就不写了,反正就是个URL路由。。

这里最后的那个 genUniqueKey()是为了当时功能需求,是一个自动生成唯一主键的函数,逻辑都在前面

结果:

我设置了 500个线程同时运行,明显的出现了 订单人数  +  剩余库存   >  总库存的现象,即“超卖现象”

 

解决一:(synchronized,很难受的解决方案)

对于多线程并发处理的问题,我直接想到了通过加锁(synchronized)

就是在orderProductMockDiffUser()的函数上设置了synchronized属性,来保证每一个线程都会完整的执行这段代码,,

额,想法是好的,结果呢,也实现了不出现“超卖”的现象,但是!!!!这速度,也太慢了,,我才设置了500个线程,,足足3分钟才完事,我如果是用户。此刻一定会摔手机的。。

解决二(Redis分布式锁):

加锁:

通过使用Redis的命令:

先上代码,然后解释

public class RedisLock {
    @Autowired
	private StringRedisTemplate redisTemplate;

	/**
	 * 加锁
	 * 
	 * @param key
	 * @param value 当前时间 + 超时时间
	 * @return
	 */
	public boolean lock(String key, String value) {
		if (!redisTemplate.opsForValue().setIfAbsent(key, value)) {
			return true;
		}
		String currentValue = redisTemplate.opsForValue().get(key);
		// 如果锁过期
		if (StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
			// 获取上一个锁时间
			String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
			if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
				return true;
			}
		}
		return false;
	}
}

 

首先在这里定义一个lock()方法,来实现加锁操作。

这里,先只解释setnx的逻辑,关于getset后面会继续!

解释一下这里的逻辑:

  • 使用setnx来判断当前线程是否加锁

       这里就是使用了setnx的命令,在java中为setIfAbsent(key, value),

       通过上面给的Redis文档上可以看到,这个函数返回的是0,1;意思是如果设置一个value(对应的key不存在),那么,就set一个key-value,如果设置的这个value以及有对应的key值,那么就不执行任何操作;

       这样的话,我们就可以理解:当一个线程执行setnx返回1时,说明key原本不存在,该线程成功的得到了锁;当一个线程执行setnx返回0时,说明key已经存在,该线程抢占锁失败。

  • 锁超时,设置value值为 当前时间 + 超时时间

锁超时就是说:如果一个得到锁的线程在执行任务的过程中挂掉,没有来得及释放锁,这样,这块资源将会被永远锁住,别的线程再也进不来了。

so,这里就要要设置超时时间,以保证即使产生上述情况的线程没有被显示释放,这把锁也一定要在一段时间后自动释放

但是,setnx不支持设置超时参数,所以需要用expire命令

关于使用expire设置超时时间,上述代码没有通过这个实现,而是在service直接设置了超时时间。

==========================================分割线============================================

但是!!如果仅仅是使用setnx命令就能实现分布式加锁的话,那就错了

所以这里一会就要说getset的使用了

举个场景例子:

在一段代码中,先是进行了加锁操作,即上述代码的lock()函数(仅使用setnx指令),然后程序继续进行,突然,因为某种原因抛出了一个异常,程序在这里就终止了,然后,继续有其他线程进到这个方法时,因为setnx此时的返回为1,为false,就不会获得锁,这样的话,就会导致死锁的产生

所以,对于这种情况,就还需要getset了

  • 首先判断一下锁是否过期,即判断一下“当前时间+超时时间”即currentValue是否为空并且是小于当前时间,
  • 然后获取上一个锁的时间,进行判断,当前的currentValue是否和上一个锁的时间相等,如果相等了,就返回true,这样就可以保证有一个线程可以打开一把锁,避免了死锁。

这里再举个例子,用场景来执行一下这段代码:

此时,有两个线程,线程A和线程B,在执行到setnx的函数时,锁都被占用,然后继续进行代码,先设置此时的currentValue为A,线程A,线程B的value都是B,然后它们的锁都过期了,这样就执行到了getset的函数那里,线程A,线程B执行getset的函数时一定有先后顺序,虽然间隔很短,这里多线程的问题不多说,这样的话,oldvalue的值此时为A,然后再将Bset到key中,再然后进行下面的判断时,oldValue为A,currentValue也为A,所以返回了一个true,然后另一个线程再来执行这里,此时oldValue为B,currentValue为A,返回一个false;

所以,通过这个getset代码,可以解决了死锁的问题,又保证了多个线程进来时,只会有一个线程能拿到锁。

额,因为线程这里不能打断点调试看结果,所以,口头叙述,不知道到时候自己再回来看这块的逻辑能不能看得明白。。。

 

========================================分割线==============================================

剩下的就是解锁了。就是一个delete操作。

	/**
	 * 解锁
	 * @param key
	 * @param value
	 */
	public void unLock(String key, String value) {
		try {
			String currentValue = redisTemplate.opsForValue().get(key);
			if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
				redisTemplate.opsForValue().getOperations().delete(key);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

总结:

在我认为,分布式锁就是多台机器上多个线程对一个数据进行操作的互斥;

而Redis适合作分布式锁的原因很大程度上是因为它是单线程的。所以实现起来很符合要求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值