《黑马点评》实现一人一单功能笔记
批量获取用户token,并使用jmeter对秒杀接口进行压力测试(黑马点评)blog.csdn.net/qq_32792547…
教我怎么用Jmeter 测试的 blog.csdn.net/dingd1234/a…
修改秒杀业务,要求同一个优惠券,一个用户只能下一单。
只能怪博主大神笔记记的太全了呜呜呜我本来想自己好好总结的来着,就只能锦上添花了。
首先不建议把锁加在方法上,因为任何一个用户来了都要加这把锁,而且是同一把锁,方法之间变成串行执行,性能很差。
因此可以把锁加在用户id上,只有当id相同时才会对锁形成竞争关系。但是因为toString的内部是new了一个String字符串,每调一次toString都是生成一个全新的字符串对象,锁对象会变。
所以可以调用intern()方法,intern()方法会优先去字符串常量池里查找与目标字符串值相同的引用返回(只要字符串一样能保证返回的结果一样)。
但是因为事务是在函数执行结束之后由Spring进行提交,如果把锁加在createVoucherOrder内部其实有点小——因为如果解锁之后,其它线程可以进入,而此时事务尚未提交,仍然会导致安全性问题。
因此最终方案是把synchronized加在createVoucherOrder的方法外部,锁住的是用户id。
关于代理对象事务的问题:通常情况下,当一个使用了@Transactional注解的方法被调用时,Spring会从上下文中获取一个代理对象来管理事务。
但是如果加@Transactional方法是被同一个类中的另一个方法调用时,Spring不会使用代理对象,而是直接调用该方法,导致事务注解失效。
为避免这种情况,可以使用AopContext.currentProxy方法获取当前的代理对象,然后通过代理对象调用被@Transactional注解修饰的方法,确保事务生效。
在VoucherOrderServiceImpl中写入如下代码(注意:ctrl+alt+m可以把含有return的代码段进行提取):
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
//2.1秒杀尚未开始返回异常
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//2.2秒杀已结束返回异常
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
voucher = seckillVoucherService.getById(voucherId);
//3.判断库存是否充足
if (voucher.getStock() < 1) {
//3.1库存不足返回异常
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
/**
* 先获取锁 再完成下单
*/
/*
最终方案是把synchronized加在createVoucherOrder的方法外部
加锁的对象是用户id 所以要再外面获取用户id 然后上锁
先获取锁 提交事务 再释放锁
但是如果加@Transactional方法是被同一个类中的另一个方法调用时,Spring不会使用代理对象,
而是直接调用该方法,导致事务注解失效。
为避免这种情况,可以使用AopContext.currentProxy方法获取当前的代理对象,
然后通过代理对象调用被@Transactional注解修饰的方法,确保事务生效。
*/
synchronized (userId.toString().intern()) {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return createVoucherOrder(voucherId);
}
}
/**
* 先查询 再判断 判断完了再去扣减库存
* 然后从查询出订单到判断订单到新增订单整段逻辑加上悲观锁 需要把这段逻辑做一个封装
* 然后方法内部加上synchronized 同一个用户加一把锁 不同的用户加不同锁 根据id来加
* 但是因为事务是在函数执行结束之后由Spring进行提交,如果把锁加在createVoucherOrder内部
* 其实有点小——因为如果解锁之后,其它线程可以进入,而此时事务尚未提交,仍然会导致安全性问题。
*/
@Transactional
public Result createVoucherOrder(Long voucherId) {
//6.一人一单
Long userId = UserHolder.getUser().getId();
/*
intern()方法会优先去字符串常量池里查找与目标字符串值相同的引用返回
(只要字符串一样能保证返回的结果一样)。
这里用intern()就能确保当用户的id一样 则锁就一样
*/
//6.1查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//6.2判断是否存在
if (count > 0) {
//用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
//没买过就去创建订单
//3.2库存充足扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") //相当于set条件 set stock = stock - 1
.eq("voucher_id", voucherId) //相当于where条件 where id = ? and stock = ?
.gt("stock", 0).update();
if (!success) {
return Result.fail("库存不足!");
}
//4.创建订单,返回订单id
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");//订单id
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);//代金券id
save(voucherOrder);
return Result.ok(orderId);
}
}
Java 8 Runnable和Callable使用Lambda表达式示例(带参数)
参考文章:www.concretepage.com/java/jdk-8/…
@Component
public class RedisIdWorker {
private static final long BEGIN_TIMESTAMP = 1640995200L;
//定义序列号的位数
private static final int COUNT_BITS=32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix){
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long timeStamp = now.toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP;
//2.生成序列号
//2.1获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
//借助到 位运算
//让时间戳的值向左移动,多少位不要写死,定义一个常量就行
//这里用或运算 先让右边这个向左补齐,多出来的进行或运算保留
return timeStamp << COUNT_BITS | count;
}
}
//注入redisIdWorker
@Resource
private RedisIdWorker redisIdWorker;
//线程池
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
//Java 8 Runnable和Callable使用Lambda表达式(带参数)
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
//每当一个线程完毕就会执行一次countDown()
latch.countDown();
};
/*
等同于
Runnable task = new Runnable() {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
};
*/
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
//等待countDown()结束为止
latch.await();
long end = System.currentTimeMillis();
System.out.println(end - begin);//打印时间
}
}
转载自:https://juejin.cn/post/7370841545199583266