Redis实战:黑马点评之秒杀优化
1 分布式锁优化秒杀
1.1 问题引入
通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
1.我们将服务启动两份,端口分别为8081和8082:
2.然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
3.重新加载nginx,命令为nginx.exe -s reload
4.经过测试,最终发现在集群模式下,有多少个服务,用户最多就能下多少单,也就是说在集群模式下,我们之前使用的悲观锁失效了
为什么会出现上述现象呢?原因是由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,而每个jvm内部有一个锁监视器,用来记录当前获取锁的线程id,假设在服务器A的tomcat内部,有两个线程分别为线程1、线程2,这两个线程由于在同一个jvm上运行,且锁对象是同一个,当线程1获取锁对象时就会被锁监视器记录下来,此时线程2就无法获取锁对象了,这样也就实现了互斥锁。
但是如果现在是服务器B的tomcat内部,又有两个线程分别为线程3、线程4,这两个线程虽然和线程1、线程2运行着同样的代码,但是却是在不同的jvm上运行的,这也就意味着它们拥有不同的锁监视器,即便在第一个jvm中,锁监视器已经获取到了线程1的id,但是在第二个jvm中,锁监视器仍然是空的,这也就意味着线程3和线程4都能去获得锁,这样的话,同一部分代码,同一个锁对象,在不同的服务器上却是不同的锁,线程3与线程4能实现互斥,但是却无法和线程1与线程2实现互斥,这就是集群环境下,syn锁失效的原因。
在这种情况下,我们就需要使用分布式锁来解决这个问题。
1.2 SetNX优化
注:关于分布式锁的相关知识笔者系统地整理在了另一篇文章中:【Redis】Redis高级:分布式锁,这里只讲解代码的编写
第一步:在util包下编写ILock接口
/**
* 分布式锁父接口
*/
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后锁自动释放
* @return 返回true表示锁获取成功,返回false表示锁获取失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
第二步:编写SimpleRedisLock实现ILock(这里已解决锁误删问题和原子性问题)
public class SimpleRedisLock implements ILock {
/**
* 业务名称,由调用者创建对象时传入,目的是保证在不同的业务中都有不同的key作为锁
*/
private String name;
private StringRedisTemplate stringRedisTemplate;
/**
* 锁前缀
*/
private static final String KEY_PREFIX = "lock:";
/**
* 随机生成的线程ID前缀,这里用final保证在同一个jvm上面前缀是一致的
*/
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
/**
* 脚本对象
*/
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
/**
* 使用静态代码块加载lua脚本,在类启动的时候就将脚本加载进内存
*/
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
//设置脚本路径
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
//设置脚本返回值类型。这里随意即可
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 获取锁
* @param timeoutSec 锁持有的超时时间,过期后锁自动释放
* @return
*/
@Override
public boolean tryLock(long timeoutSec) {
//获取当前线程标识
String id = ID_PREFIX+Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
KEY_PREFIX + name,
id,
timeoutSec,
TimeUnit.MINUTES
);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
@Override
public void unlock() {
//获取当前线程表示
String id = ID_PREFIX+Thread.currentThread().getId();
//调用lua脚本执行锁释放操作
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name), //需要释放的锁的id
ID_PREFIX + Thread.currentThread().getId() //需要进行判断的线程标识
);
}
}
第三步:在resources目录中新建一个名为unlock.lua的脚本,内容如下:
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 左右盲.
--- DateTime: 2022/9/19 21:21
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
第四步:改造秒杀代码,将syn锁替换成我们自定义的分布式锁
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 实现秒杀下单
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
//获取秒杀券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判断秒杀券是否存在
if(seckillVoucher == null){
return Result.fail("秒杀券不存在");
}
//判断秒杀是否开始或是否结束
LocalDateTime now = LocalDateTime.now();
//如果现在时间在开始时间之前或者在结束时间之后说明秒杀活动未开始或者已结束
if(now.isBefore(seckillVoucher.getBeginTime())||now.isAfter(seckillVoucher.getEndTime())){
return Result.fail("不在活动时间内");
}
//判断库存是否足够
if(seckillVoucher.getStock() < 1){
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
//创建锁对象,由于这里我们希望保证一人一单,因此针对userId来创建锁
SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean tryLock = redisLock.tryLock(1000l);
//获取锁对象,如果失败则表示此时该用户已经在下单了
if(!tryLock){
return Result.fail("不允许重复下单");
}
try {
//由spring帮我们创建当前类的代理对象,由代理对象来调用方法
//由于代理对象是spring创建的,自然就能进行事务的管理了
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} catch(Exception e){
throw new RuntimeException(e.getMessage());
}finally {
//释放锁对象
redisLock.unlock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//判断该用户有没有下单过
Integer count = query()
.eq("user_id", UserHolder.getUser(