工作中,有很多业务场景需要实现类似秒杀,抢购的功能,这种短时高并发的场景尤其需要注意防止商品出现超卖的问题,一旦超卖,各位猿们就准备删库跑路把~~~~
大家可能会想,synchronized大法!别,想想拉肚子时厕所满坑的尴尬~~~~
这时可以了解下用redis实现分布式锁,它能够实现分布式环境下的数据一致性,其本质是利用了redis是单线程的,或者说redis的网络模块是单线程的,其他模块还是用多线程的!
实现的思路主要依靠redis的两个原子性命令:SETNX GETSET
SETNX:将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作
GETSET:将给定 key 的值设为 value ,并返回 key 的旧值(old value)
贴一下我的代码:
@RestController
@RequestMapping(value = "test")
public class MsService {
@Autowired
private RedisService redisService;
//锁超时时间,防止线程在入锁以后,无限的执行等待
private int expireMsecs = 3 * 1000;
//锁等待时间,防止线程饥饿
private int timeoutMsecs = 10 * 1000;
private volatile boolean locked = false;
//设置锁的唯一key
String lockKey = "good";
//模拟初始商品数
int n = 50;
/***
* 抢购代码
* @return
*/
@RequestMapping(value = "/thread",method = RequestMethod.GET)
public boolean seckill() {
try {
if(n<=0) {
System.out.println("手慢拍大腿,抢光啦222222222!");
return false;
}
if (this.lock()) {
//修改库存
if(n-1>=0) {
n--;
System.out.println("库存数量:"+n+" 成功!!!"+Thread.currentThread().getName() + "获得了锁");
}else {
System.out.println("手慢拍大腿,抢光啦!");
return false;
}
this.unlock();
return true;
}else System.out.println("换个姿势再试试");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,
// 操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做
this.unlock();
}
return false;
}
public synchronized boolean lock() throws InterruptedException {
//SETNX是将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
//GETSET key value,将给定 key 的值设为 value ,并返回 key 的旧值(old value)
//redis是单线程的,这里的单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程
long expires = System.currentTimeMillis() + expireMsecs + 1;
String expiresStr = String.valueOf(expires); // 锁到期时间
//满足下面的判断即该线程成功抢到了锁
if (redisService.setnxByKey(lockKey, expiresStr).toString().equals("1")) {
// lock acquired
locked = true;
return true;
}
//没有抢到锁的线程需要判断锁是否已经超时,若超时需要释放锁
String currentValueStr = redisService.getByKey(lockKey); //当前获取锁的线程的到期时间
//判断是否超时
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
// lock is expired
//为当前锁设置新锁到期时间,并且获取到旧锁到期时间
String oldValueStr = redisService.getSetByKey(lockKey, expiresStr);
//此处GETSET为原子性操作(类似抛绣球),若两个线程AB同时到这里,只有一个线程能够获取到旧锁的到期时间(假设为A),
//此时B获取到的值是旧锁的到期时间+A锁的到期时间
//下面的判断只有A锁满足条件,但是A的到期时间会被B的到期时间所覆盖,因为时间相差很少,所以一般可以接受
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
locked = true;
return true;
}
}
return false;
}
/**
* Acqurired lock release.
*/
public synchronized void unlock() {
if (locked) {
redisService.del(lockKey);
locked = false;
}
}
}
具体的逻辑,注释已经很清楚了!这里没有加入数据库,大家自行加入,抢购成功时加入修改库存,查询库存的代码即可
最后,我加入了Jmeter进行测试,jmx文件我也已经上传到 ↓↓↓
全部代码在这:中国最大男性交友网站,欢迎Fork
版权声明:本文为wangtaojiushiwo原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。