一 前言:
在同一个JVM中多个线程争抢同一个资源时可以使用JUC提供的一些锁或者JDK自带的Lock,synchronized关键字等解决并发多线程问题. 但是在多JVM情况下,这些东东都无力回天啦! 这个时候是不是想到要用分布式锁来解决问题了.
二 简介:
分布式锁一般分为以下三类
- 基于数据库原子性做分布式锁
- 基于redis做分布式锁
- 基于zookeeper做分布式锁
三 多线程例子
假设我们要多个线程扣减库存时 --->咋们起始的代码 也算是咋们的业务逻辑:
@RequestMapping(value = "/stock_deduct")
public String deductStockDeduct() {
String bestpayLockKey = "bestpayLock";
//库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("bestpay"));
log.info("redisStock: {}", stock);
if (stock > 0) {
//如果库存大于0则进行减1操作
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("bestpay", realStock + "");
log.info("扣减成功,剩余库存:{}", realStock);
} else {
log.info("扣减失败,库存不足!");
}
return "success";
}
这时我们库存设为50 并用JMeter压测200个线程并发时,发现库存还有45个,这时并发线程安全的问题就来了.
为啥? --->主要原因我们redis处理速度太快了,导致没有锁的情况下很多进程争抢一个资源时出现无序混乱的情况.
四 基于MYSQL实现分布式锁
引荐:<<基于mysql实现分布式锁>>
基于mysql数据库实现分布式锁主要有两种方式:一种是基于数据库表实现的乐观锁和悲观锁,另一种是基于Mysql自带的悲观锁;
Mysql实现分布式悲观锁:直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
利用for update加显式的行锁,这样就能利用这个行级的排他锁来实现分布式锁了,同时unlock的时候只要释放commit这个事务,就能达到释放锁的目的。
五 基于ZK实现分布式锁
六 基于Redis实现分布式锁
redis命令大全 中有get,set,setnx,del,getset等方法.
回顾下我们的起始业务代码,
咋办?--->奥 那我们加把锁呗. 我们在方法上加上synchronized关键字, 诶 发现完全可以解决刚刚的并发问题没有超卖,少卖等情况.
synchronized作用于静态方法和非静态方法的区别:
非静态方法:
* 给对象加锁(可以理解为给这个对象的内存上锁,注意 只是这块内存,其他同类对象都会有各自的内存锁),这时候
* 在其他一个以上线程中执行该对象的这个同步方法(注意:是该对象)就会产生互斥
* 静态方法:
* 相当于在类上加锁(*.class 位于代码区,静态方法位于静态区域,这个类产生的对象公用这个静态方法,所以这块
* 内存,N个对象来竞争), 这时候,只要是这个类产生的对象,在调用这个静态方法时都会产生互斥
接下来小编制造麻烦了,在本地搭建两台Tomcat端口分别是8081,8082 用nginx负载均衡 分发到这两台机器上.
在conf文件nginx.conf中http中增加配置:使其redislock域名会均匀分发到两台Tomcat上.
1.重启命令nginx -s reload 2.停止命令nginx -s stop 3.启动命令start nginx
然后发现synchronized加了也没用.还是有并发问题.
咋办--->这时分布式锁就派上用场了!
public String deductStockDeduct() {
String bestpayLockKey = "bestpayLock";
try {
final Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(bestpayLockKey, "bestpay");
if (!getLock) {
return "end";
} else {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("bestpay"));
log.info("redisStock: {}", stock);
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("bestpay", realStock + "");
log.info("扣减成功,剩余库存:{}", realStock);
} else {
log.info("扣减失败,库存不足!");
}
}
} catch (NumberFormatException e) {
log.error("getLock error------");
} finally {
stringRedisTemplate.delete(bestpayLockKey);
}
return "success";
}
改为上面代码后发现没有超卖情况了, 但仔细想想在实际业务情况下是不是有很多问题.. 他把没拿到锁的线程直接抛给上帝了...
咋办---> 如果我们没拿到锁就一直循环去获取锁
while (true) {
getLock = stringRedisTemplate.opsForValue().setIfAbsent(bestpayLockKey, "bestpay");
if (getLock) {
stringRedisTemplate.expire(bestpayLockKey,10,TimeUnit.SECONDS);
break;
}
}
发现业务逻辑已经可以实现了. 但是发现性能很差, 有什么可改进的地方吗?
发现while循环导致没获取到锁的会一直尝试获取锁,资源消耗严重 我们直接让没获取到锁的线程休眠2ms左右,发现瞬间爽了很多.
其实2.6以上的jedis其实可以直接用.set方法,原子执行set操作. 以下可以考虑lua脚本执行
可以看到,我们加锁就一行代码:
jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用
UUID.randomUUID().toString()
方法生成。第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间
干活满满..有问题大家一起探讨 qq260179415 ,后面大家想想可能还会出现什么不可预估的情况.. (lua脚本,redission 等)
现在看起来想要实现非常完美的redis分布式锁是不是不是很难!
本文主体逻辑图:
Demo下载:https://download.csdn.net/download/qq_28953809/11144179
作者简介:就职于甜橙金融信息技术部,负责java后端开发工作,喜欢研究新的技术服务于业务需求,保证服务的高并发,高可用,高性能.