redis的锁机制和秒杀问题实践
最近学习了redis的锁机制并且进行了秒杀案例解决超卖的实践
multi/exec/discard
Redis Multi 命令用于标记一个事务块的开始。
事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。
总结的说,redis是单线程的,每一 个命令都是在队列中顺序执行的,而Multi会让一系列指令按照顺序执行,且不允许其他指令插队,让这一串指令具备了原子性
multi:开启一个事务,接下来输入的指令(本客户端内)将不会返回值,直到调用了exec命令
exec: 执行一个事务,返回数组,即multi后所有指令的返回值
discard:抛弃这个事务
乐观锁与watch命令
redis的锁机制是基于乐观锁,乐观锁即为一个字段增加了版本号,每次更新都会原子性地把版本号加一,监听这个版本号后如果发现最终更新与这个版本号不一致将会不执行这次更新。
watch与multi
redis中watch命令是用乐观锁实现的锁机制,需要和multi命令配合使用。
使用方法:
watch key
multi
command 1
command 2
...
exec
效果:watch key即用乐观锁监听这个key,如果multi中的指令执行时会修改这个key,若发现key被别人修改过就会放弃这次事务。总而言之就是watch和multi之间key被修改就会放弃这次事务。
秒杀问题的解决
背景
为了加快抢购速度,避免数据库系统压力瞬间增加,会在抢购时将数据库中商品库存读入redis,所有抢购操作在redis中操作,这个小demo就是模拟redis中抢购的过程。
设计思路
用户从redis中读取商品余额,如果不存在key则说明抢购还没开始。开始抢购时向redis中添加商品余额,用户抢购到商品后库存减一,并且将该用户的id加入抢购名单set集合中,我的代码是由redisTemplate写的
可能出现的问题有:
- 超卖:如果有两个线程同时进入了减少库存的代码且没做处理就可能导致库存卖到负值
- 重复买:同一个用户连续发送多个请求且同时进入了购买逻辑
- 剩余库存:如果用乐观锁做了控制导致用户很容易就抢购失败了,用户体验会很糟糕。这里用自旋的方式降低抢购失败发生的概率
代码如下
@PostMapping("/kill")
public String testSecKill(@RequestBody SecKillDto secKillDto) {
Integer prodId = secKillDto.getProdId();
Integer userId = secKillDto.getUserId();
String prodKey = "sk:prod:" + prodId + ":int";
String userKey = String.format("sk:buylist:%d:set:int", prodId);
Integer prodNum = (Integer) redisTemplate.boundValueOps(prodKey).get();
if(prodNum == null) {
return "还没有开始";
}
for(int i = 0; i < casTime; i++) {
Object exec = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.watch(prodKey);
if(redisTemplate.boundSetOps(userKey).isMember(userId)) {
return "不能重复抢";
}
if(((Integer) redisTemplate.boundValueOps(prodKey).get()).compareTo(0) <= 0) {
return "已经抢光了";
}
operations.multi();
operations.boundValueOps(prodKey).decrement();
operations.boundSetOps(userKey).add(userId);
List exec1 = operations.exec();
return exec1;
}
});
if(exec instanceof List && (exec == null || ((List)exec).size() == 0)) {
System.out.println("失败");
} else if(exec instanceof String) {
return (String) exec;
} else {
return "抢购成功";
}
}
return "抢购失败";
}
说明:一定要在watch后判断是否重复抢和是否抢光,因为如果在watch之前做判断,判断和watch之间可能值被减少,实际watch的值比判断的值要小,导致有可能watch的就是0。watch之后再判断可以保证watch之后如果是0就不执行
使用JMeter做压测
1. 下载JMeter并解压
http://jmeter.apache.org/download_jmeter.cgi
2. 新建测试
打开JMeter的GUI,即JMeter.bat,新建测试,就不多赘述了
压测后看看结果,有没有超卖或者重复抢购,应该是没有的