分布式锁高并发场景下的典型问题与优化实践
背景 开启课堂或者刷新课堂界面时会调用接口发送邀请链接公告,后台要限制一个课堂只能发送一次。 现象 同一个课堂出现了多条公告记录 解析 直接看代码,使用了分布式锁进行加锁校验,锁等待时间为0,也就是获取不到锁立刻返回。锁超时时间为课堂的超时时间,为12小时。 1、猜想一:发送公告有重试逻辑,有可能是重试逻辑有问题导致的。 在Google的接口调用中,使用动态代理进行接口的统一处理,如果调用Google接口返回了401状态码,就需要刷新AccessToken然后重新调用,如果刷新AccessToken失败就需要用户重新授权。 初步看代码逻辑没有问题,我们可以查看应用日志,如果是重试的问题导致的,那么这两次发送公告都应该是同一个请求,也就会是同一个TraceId,可以看到这两次发送成功是属于两个不同的请求,TraceId也是不一样的,那么可以排除这个猜想。 2、猜想二:锁没有生效,有并发问题。 我们使用 Jmeter 对这个接口进行压力测试,开启100个线程,结果是只有一个线程返回成功,其他线程都返回10304业务状态码,表示已经发送过公告了,而且始终没办法复现,返回成功状态码的请求始终只有一个。 3、猜想三:第一次获取锁成功了,后面删除了锁,所以第二次获取锁又成功了 查看Redis数据库也能看到锁对应的数据,TTL剩余9379秒,通过时间计算,可以得出这个Key的设置时间为9:49,跟第一次请求的时间是一致的,所以第二次获取锁的时候,这个锁是存在的。 4、猜想四:从以上的现象可以看出,这不是并发问题导致的,而且是属于不同的请求,两个请求基本没有什么关联,间隔也有8分钟左右。第二次获取锁的时候,锁的key也确实存在,确实是重复获取到了锁,那么基本确定只有一种可能,这是可重入锁,如果两个请求是同一个线程,那么就能重复获取到锁。查看应用日志,可以发现这两个请求确实属于同一个线程。 验证猜想:将Tomcat的工作线程数设置为1,这样每次请求都会是同一个请求,结果是每次请求都发送成功。 那么压力测试为什么复现不了这个问题呢,原因是压力测试时,100个请求基本同一时间发出,而Tomcat默认的最大线程池数量为200个线程,所以这100个请求都属于不同的线程。 解决方案 方案一:自实现不可重入锁,使用 setnx 指令简单实现。 方案二:加一层校验,在加锁之前判断这个 Key 是否存在。 这两种方案都可以,选择一种即可。 附 分布式锁加锁流程图