基于商品显示秒杀-一人一单业务_xzm_的博客-CSDN博客改进
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
分布式锁的五个基本要求:多进程可见,互斥,高可用,高性能,安全性
三种实现方式
redis
1.创建获取锁删除锁的工具类
public interface ILock {
/**
* 获取锁
* @param timeoutSec 自动超时时间
* @return
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
public String name;
public StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX="lock:";
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
long thread = Thread.currentThread().getId();
//获取锁
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, thread + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
@Override
public void unlock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
2.修改代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
//查询优惠卷
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//秒杀尚未开始
return Result.fail("秒杀尚未开始,请耐心等待");
}
//判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
//秒杀已经结束
return Result.fail("本次秒杀已经结束");
}
//秒杀处于正常时间段
//判断库存是否充足
if (voucher.getStock()<1) {
//库存不足
return Result.fail("本次秒杀已经被抢完");
}
//库存充足
//从拦截器中获取用户id
Long userId = UserHolder.getUser().getId();
//对相同的id进行加锁
// synchronized(userId.toString().intern()) {
//获取代理对象
// IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//在service中添加createVoucherOrder方法,也可利用idea代码修复功能自动添加
// return proxy.createVoucherOrder(voucherId);
// return createVoucherOrder(voucherId);
// }
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁
boolean tryLock = lock.tryLock(500);
if (!tryLock) {
//获取锁失败
return Result.fail("每人仅限一单");
}
//获取锁成功
try {
return createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//从拦截器中获取用户id
Long userId = UserHolder.getUser().getId();
/**
* 进行判断,一人一单
*/
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
//用户已经购买过商品
return Result.fail("每位用户仅限购一单");
}
//扣减库存
boolean update = iSeckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
//判断库存扣减是否成功
if (!update) {
//库存扣减失败,返回信息
return Result.fail("库存不足");
}
//库存扣减成功,添加订单信息
VoucherOrder voucherOrder = new VoucherOrder();
//添加订单id
//使用订单生成器生成id
long id = redisIdWorker.nextId("order");
voucherOrder.setId(id);
//添加用户id
voucherOrder.setUserId(userId);
//添加消费卷id
voucherOrder.setVoucherId(voucherId);
//添加到数据库
boolean save = this.save(voucherOrder);
if (!save) {
//添加失败
return Result.fail("下单失败");
}
return Result.ok(id);
}
}
测试结果:实现了多个服务器下的一人一单
现阶段存在问题:当线程阻塞时间超过setnx的自动过期时间时可能导致一人多单和setnx的key误删情况
优化误删问题
思路:
实现:
修改工具类
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
public String name;
public StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.fastUUID().toString(true)+"-";
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String thread = ID_PREFIX+Thread.currentThread().getId();
//获取锁
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, thread , timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
@Override
public void unlock() {
//获取锁标识
String thread = ID_PREFIX+Thread.currentThread().getId();
//获取redis中的值
String s = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (thread.equals(s)){
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
}
现阶段依旧存在误删除的问题
逻辑:
解决方法:让判断锁标识与释放锁保持原子性
Lua脚本
解决方法
1.创建nulock.lua文件
2.编写lua脚本
-- 获取锁中的线程标识 get key
local id=redis.call('get',KEYS[1])
--比较线程标识与锁中的标识是否一致
if id == ARGV[1] then
--释放锁
return redis.call('del',KEYS[1])
end
return 0
3.修改unlock方法
//创建接收lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
//在静态方法中初始化UNLOCK_SCRIPT
static {
UNLOCK_SCRIPT=new DefaultRedisScript<>();
//设置接收lua脚本文件
UNLOCK_SCRIPT.setLocation(new ClassPathResource("nulock.lua"));
//设置返回值类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public void unlock() {
//使用lua脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX+Thread.currentThread().getId()
);
}
到目前为止已经可以做到生产可用
redis分布式锁的优化
需要优化的问题:不可重入,不可重试,超时释放,主从一致
解决方法:使用redis的框架redisson实现分布式锁
使用方法:
1.引入依赖
<!-- redis框架redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2.编写配置文件
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.2.182:6379").setPassword("123456");
//创建Redissonclient对象
return Redisson.create(config);
}
}
3.修改业务类(仅进行注入了RedissonClient 和创建锁对象和trylock的参数)
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.Voucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IVoucherService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
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;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
//查询优惠卷
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//秒杀尚未开始
return Result.fail("秒杀尚未开始,请耐心等待");
}
//判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
//秒杀已经结束
return Result.fail("本次秒杀已经结束");
}
//秒杀处于正常时间段
//判断库存是否充足
if (voucher.getStock()<1) {
//库存不足
return Result.fail("本次秒杀已经被抢完");
}
//库存充足
//从拦截器中获取用户id
Long userId = UserHolder.getUser().getId();
//对相同的id进行加锁
// synchronized(userId.toString().intern()) {
//获取代理对象
// IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//在service中添加createVoucherOrder方法,也可利用idea代码修复功能自动添加
// return proxy.createVoucherOrder(voucherId);
// return createVoucherOrder(voucherId);
// }
//创建锁对象
// SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁
boolean tryLock = lock.tryLock();
if (!tryLock) {
//获取锁失败
return Result.fail("每人仅限一单");
}
//获取锁成功
try {
return createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//从拦截器中获取用户id
Long userId = UserHolder.getUser().getId();
/**
* 进行判断,一人一单
*/
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
//用户已经购买过商品
return Result.fail("每位用户仅限购一单");
}
//扣减库存
boolean update = iSeckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
//判断库存扣减是否成功
if (!update) {
//库存扣减失败,返回信息
return Result.fail("库存不足");
}
//库存扣减成功,添加订单信息
VoucherOrder voucherOrder = new VoucherOrder();
//添加订单id
//使用订单生成器生成id
long id = redisIdWorker.nextId("order");
voucherOrder.setId(id);
//添加用户id
voucherOrder.setUserId(userId);
//添加消费卷id
voucherOrder.setVoucherId(voucherId);
//添加到数据库
boolean save = this.save(voucherOrder);
if (!save) {
//添加失败
return Result.fail("下单失败");
}
return Result.ok(id);
}
}
原理: