案例地址:
https://github.com/heixhei/demos
今天程序员小白被安排做一个秒杀的功能,小白迅速的打开了idea准备大展身手,不到几分钟,小白高兴的想把代码推送部署上去了,师傅看到小白这么开心,看了看小白的屏幕,气的脸色通红,直呼我没教过你这样的徒弟,我们瞧一瞧小白的又闹出了什么笑话。
线程不安全-导致超卖
小白看到秒杀后,迅速写下了如下代码:
/**
* 线程不安全--导致超卖
*/
public class V1
{
private static Long stock = 1L;
/**
* 用户下单
*/
public static void placeOrder() throws InterruptedException {
if (stock > 0) {
Thread.sleep(100);
stock--;
System.out.println(Thread.currentThread().getName() + "秒杀成功");
} else {
System.out.println(Thread.currentThread().getName()+"秒杀失败,库存不足");
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
placeOrder();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(stock);//stock=0
}
}
我们看到小白犯了一个低级错误,没有考虑线程安全,导致商品超卖了。
用户对数据读取出现了脏读。如下图所示,所有用户可以同时对一个共有变量进行改写,造成了数据混乱。
同步锁-分布式集群导致超卖
小白脑袋转了转,对呀这不是典型的并发场景吗?几分钟后,小白重新写了新的代码,如下:
/**
* 同步锁--分布式集群导致超卖
*/
public class V2 {
private static Long stock = 1L;
/**
* 用户下单
*/
public static void placeOrder() throws InterruptedException {
synchronized (stock) {
if (stock > 0) {
Thread.sleep(100);
stock--;
System.out.println(Thread.currentThread().getName() + "秒杀成功");
} else {
System.out.println(Thread.currentThread().getName()+"秒杀失败,库存不足");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(()->{
try {
placeOrder();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}
为了以防万一,小白还做了压力测试,都没出现超卖问题了。师傅看到后,感慨道;”蟹bro,Do you know BAT?我们可是大厂,你要我们分布式集群怎么办?“,小白摸着脑袋问道:”分布式集群有什么关系吗?“
Redis原生实现分布式锁
是啊,分布式集群有什么关系吗?
下图,我们可以看到,当我们的一个线程对某一资源设置同步锁后,其他线程都需要等待其解锁后在争抢资源,随着用户量增加,该系统性能达到瓶颈,需要进行水平扩展。
我们用到了Nginx进行反向代理和负载均衡,经过压测后,虽然性能上来了,可这时又出现超卖问题。synchronized它不香吗为啥还要用分布式锁?
我们可以看到,由于同步锁属于JVM级别的锁,只能锁住当前进程内的资源,分布式集群部署下,又会出现脏读。小白深夜找资料学习,终于有了思路,使用Redis和Zookeeper实现分布式锁,因为项目中本身用到了Redis,所以我们使用Redis实现。浅析redis setIfAbsent的用法及在分布式锁上的应用及同步锁的缺陷
代码如下:
/**
* redis原生实现分布式锁
*/
@SpringJUnitConfig(classes =RedisConfig.class)
public class V3 {
public static Long stock = 1L;
public static final String LOCK_KEY = "lock::productId";
@Autowired
RedisTemplate redisTemplate;
/**
* 下单
*/
public void placeOrder() {
//加上同步锁
Boolean flag = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, "1", 10, TimeUnit.SECONDS);
try {
if (flag) {
if (Thread.currentThread().getName().equals("Thread-1")) {
throw new RuntimeException("人为异常");
}
if (stock > 0) {
Thread.sleep(100);
stock--;
System.out.println(Thread.currentThread().getName() + "秒杀成功");
} else {
System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");
}
} else {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "重试");
placeOrder();
}
} catch (Exception exception) {
System.out.println(Thread.currentThread().getName()+"异常");
exception.printStackTrace();
}
finally {
redisTemplate.delete(LOCK_KEY);
}
}
@Test
public void main() throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(() -> {
placeOrder();
});
thread.start();
thread.join();
}
}
}
上述代码使用Redis原生的SETNX来实现分布式锁,当服务器访问Redis使用SETNX放回1时,表示该资源没有被占用,返回0时,表示该资源被占用,从达到加锁的效果。是不是很巧妙。
但我们要注意,
-
当锁过期时线程还在处理业务中
-
当处理完释放其他线程的锁
也就是线程-1业务还在执行,锁并没有释放,但已经过期了,其他线程就可以设立新的锁,当线程-1释放锁时,却是释放其他线程的锁
解决方法:
-
加长时间,并添加子线程每10秒确认线程是否在线,在线则将过期时间重设
-
给锁加唯一ID(UUID)
Redisson 实现分布式锁
看完这时,小白又想了想,这虽然是个好办法,但是逻辑有点复杂,而且还需要自己保证代码的健壮性太困难了,有没有现有的组件来实现呢?小白发现了Redisson提供了这个功能。代码如下:
/**
* Redisson 实现分布式锁
*/
@SpringJUnitConfig(classes = {RedissonAutoConfiguration.class, RedisConfig.class})
public class V4 {
public static Long stock = 1L;
public static final String LOCK_KEY = "lock::productId";
@Autowired
private RedissonClient redisson;
/**
* 用户下单
*/
public void placeOrder() {
RLock lock = redisson.getLock(LOCK_KEY);
lock.lock();
try {
//创建人为异常
if (Thread.currentThread().getName().equals("Thread-2")) {
throw new RuntimeException("人为异常");
}
if (stock > 0) {
Thread.sleep(100);
stock--;
System.out.println(Thread.currentThread().getName() + "秒杀成功");
} else {
System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");
}
} catch (Exception ex) {
System.out.println(Thread.currentThread().getName() + "异常:");
ex.printStackTrace();
} finally {
lock.unlock();
System.out.println(stock);
}
}
@Test
public void main() throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(() -> {
placeOrder();
});
thread.start();
thread.join();
}
}
}
下图是Redisson实现分布式锁的底层流程图:
但是Redis主从集群有一个问题,当资源加锁成功后,Master节点挂了怎么办?
因为Redis主从节点是AP架构,加锁后,不会立即同步给从节点,导致数据丢失,并没有加锁成功。小白这时又看到一个叫做RedLock的东西。
RedLock实现高可用分布式锁
RedLock使所有的Redis节点都完成加锁
代码如下:
/**
* redLock 实现分高可用布式锁
*/
@SpringJUnitConfig(classes ={RedissonAutoConfiguration.class,RedisConfig.class} )
public class V5 {
public static Long stock=1L;
public static final String LOCK_KEY="lock::productId";
@Autowired
RedissonClient redisson;
/**
* 下单
*/
public void placeOrder() {
// 这里需要不同的redssion客户端,配置连接到不同的redis服务器
RLock lock = redisson.getLock(LOCK_KEY);
RLock lock2 = redisson.getLock(LOCK_KEY);
RLock lock3 = redisson.getLock(LOCK_KEY);
//
RedissonRedLock redissonRedLock = new RedissonRedLock(lock,lock2,lock3);
redissonRedLock.lock(30,TimeUnit.SECONDS);
try {
if (Thread.currentThread().getName().equals("Thread-1")) {
throw new RuntimeException("人为异常!");
}
if (stock > 0) {
Thread.sleep(100); //模拟执行业务...
stock--;
System.out.println(Thread.currentThread().getName() + "秒杀成功");
} else {
System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");
}
} catch (Exception ex) {
System.out.println(Thread.currentThread().getName() + "异常:");
ex.printStackTrace();
}
finally {
lock.unlock();
System.out.println(stock);
}
}
@Test
public void main() throws InterruptedException {
for (int i=0;i<3;i++){
Thread thread = new Thread(() -> {
placeOrder();
});
thread.start();
thread.join();
}
}
}
编写不易,求个三连
感谢徐庶老师讲解