初步实现
主要业务流程:
在Utils创建ILock接口:
package com.hmdp.utils; public interface ILock { /** * 尝试获取锁 * @param timeoutSec 锁持有的超时时间,过期后自动释放 * @return true代表获取锁成功;false代表获取锁失败 */ boolean tryLock(long timeoutSec); /** * 释放锁 */ void unlock(); }创建实现类SimpleRedisLock并实现ILock接口:
package com.hmdp.service.impl; import com.hmdp.utils.ILock; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate stringRedisTemplate; public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } private static final String KEY_PROFIX = "lock:"; /** * 尝试获取锁 * @param timeoutSec 锁持有的超时时间,过期后自动释放 * @return true代表获取锁成功;false代表获取锁失败 */ @Override public boolean tryLock(long timeoutSec) { //获取线程标示 long threadId = Thread.currentThread().getId(); //获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PROFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } /** * 释放锁 */ @Override public void unlock() { stringRedisTemplate.delete(KEY_PROFIX + name); } }将VoucherOrderServiceImpl修改为如下代码
import com.hmdp.utils.UserHolder; import org.springframework.aop.framework.AopContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, Vou private ISeckillVoucherService seckillVoucherService; @Autowired private RedisIdWorker redisIdWorker; @Autowired private StringRedisTemplate stringRedisTemplate; @Override public Result secVoucherKill(Long voucherId) { public Result secVoucherKill(Long voucherId) { //8.返回订单id Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()) { //创建锁对象 SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate); //获取锁 boolean isLock = lock.tryLock(1200); //判断是否获取成功 if (!isLock){ return Result.fail("不允许重复下单"); } try { //获取代理对象(事务) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }finally{ //释放锁 lock.unlock(); } } @Transactional public Result createVoucherOrder(Long voucherId) { //5.一人一单 Long userId = UserHolder.getUser().getId(); //查询订单 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0){ return Result.fail("该优惠券每人仅限一单!"); } //6.扣减库存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId).gt("stock",0) .update(); if (!success){ //扣减失败 return Result.fail("库存不足"); } //7.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); //7.1订单id long orderID = redisIdWorker.nextId("order"); voucherOrder.setId(orderID); //7.2用户id voucherOrder.setUserId(userId); //7.3优惠券id voucherOrder.setVoucherId(voucherId); //7.4创建订单 save(voucherOrder); return Result.ok(orderID); } }解决redis分布式锁的误删问题:
误删问题出现的原因:
线程A获取锁成功后,处理自己的业务逻辑时由于种种原因可能会出现处理缓慢或程序休眠等情况,而redis锁在获取时设置的过期时间已经过期,线程B现在可以进行获取锁和处理自身业务的操作,此时线程A的业务执行完成开始进行释放锁,而现在redis中的锁属于线程B,此时就会出现误删问题
解决方案:
在获取锁时添加线程标示(UUID+线程id),在释放锁时进行标示判断,一致则可以释放;不一致则说明当前线程的标示早已过期,不需要再进行手动删除
代码逻辑改动如下:
/** * 尝试获取锁 * @param timeoutSec 锁持有的超时时间,过期后自动释放 * @return true代表获取锁成功;false代表获取锁失败 */ @Override public boolean tryLock(long timeoutSec) { //获取线程标示 String threadId = ID_profix + Thread.currentThread().getId(); //获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PROFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); }/** * 释放锁 */ @Override public void unlock() { //获取线程标示 String threadId = ID_profix + Thread.currentThread().getId(); //获取锁中的标示 String id = stringRedisTemplate.opsForValue().get(KEY_PROFIX + name); //判断标示是否一致 if (threadId.equals(id)){ //释放锁 stringRedisTemplate.delete(KEY_PROFIX + name); } }基于Lua脚本确保释放锁操作的原子性
在线程A释放锁操作时,判断线程表示与锁标示一致后,进行释放操作可能会出现业务阻塞,锁到期自动释放;此时线程B可以获取锁操作成功,如果线程A业务恢复就可能删掉其他线程的锁
实现方案:
编写Lua脚本
--- 比较线程标示与锁中的标示是否一致 if (redis.call('get', KEYS[1]) == ARGV[1] ) then ---释放锁 redis.call('del',KEYS[1]) end return 0修改释放锁逻辑
/** * 释放锁 */ public void unlock(){ //调用lua脚本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PROFIX + name), ID_profix + Thread.currentThread().getId()); }