当前位置:   article > 正文

redis之:秒杀功能_synchronized (("" + userid).intern())

synchronized (("" + userid).intern())

一、超卖问题

乐观锁解决超卖问题

1、 版本号法

2、CAS法

  1. @Service
  2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  3. @Resource
  4. private ISeckillVoucherService seckillVoucherService;
  5. @Override
  6. public Result seckillVoucher(Long voucherId) {
  7. //1.查询优惠券信息
  8. SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
  9. //2.判断秒杀是否开始
  10. if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
  11. return Result.fail("秒杀尚未开始");
  12. }
  13. //3.判断秒杀是否结束
  14. if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
  15. return Result.fail("秒杀已经结束");
  16. }
  17. //4.判断库存是否充足
  18. if (seckillVoucher.getStock() < 1) {
  19. return Result.fail("库存不足");
  20. }
  21. //5.扣减库存
  22. boolean success = seckillVoucherService.update()
  23. .setSql("stock = stock - 1")//set stock = stock - 1
  24. .eq("voucher_id", voucherId).eq("stock",seckillVoucher.getStock())//where id = ? and stock = ?
  25. .update();
  26. if (!success) {
  27. return Result.fail("库存不足");
  28. }
  29. //6.创建订单
  30. VoucherOrder voucherOrder = new VoucherOrder();
  31. //6.1订单id
  32. voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
  33. //6.2用户id
  34. Long id = UserHolder.getUser().getId();
  35. voucherOrder.setUserId(id);
  36. //6.3代金券id
  37. voucherOrder.setVoucherId(voucherId);
  38. save(voucherOrder);
  39. //7.返回订单id
  40. return Result.ok();
  41. }
  42. }

没有超卖,为什么失败太多?

原因:库存充足时,只要改了stock就是取消。所以只要stock > 0就行

  1. @Service
  2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  3. @Resource
  4. private ISeckillVoucherService seckillVoucherService;
  5. @Override
  6. public Result seckillVoucher(Long voucherId) {
  7. //1.查询优惠券信息
  8. SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
  9. //2.判断秒杀是否开始
  10. if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
  11. return Result.fail("秒杀尚未开始");
  12. }
  13. //3.判断秒杀是否结束
  14. if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
  15. return Result.fail("秒杀已经结束");
  16. }
  17. //4.判断库存是否充足
  18. if (seckillVoucher.getStock() < 1) {
  19. return Result.fail("库存不足");
  20. }
  21. //5.扣减库存
  22. boolean success = seckillVoucherService.update()
  23. .setSql("stock = stock - 1")//set stock = stock - 1
  24. .eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
  25. .update();
  26. if (!success) {
  27. return Result.fail("库存不足");
  28. }
  29. //6.创建订单
  30. VoucherOrder voucherOrder = new VoucherOrder();
  31. //6.1订单id
  32. voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
  33. //6.2用户id
  34. Long id = UserHolder.getUser().getId();
  35. voucherOrder.setUserId(id);
  36. //6.3代金券id
  37. voucherOrder.setVoucherId(voucherId);
  38. save(voucherOrder);
  39. //7.返回订单id
  40. return Result.ok();
  41. }
  42. }

二、一人一单

  1. @Service
  2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  3. @Resource
  4. private ISeckillVoucherService seckillVoucherService;
  5. @Override
  6. public Result seckillVoucher(Long voucherId) {
  7. //1.查询优惠券信息
  8. SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
  9. //2.判断秒杀是否开始
  10. if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
  11. return Result.fail("秒杀尚未开始");
  12. }
  13. //3.判断秒杀是否结束
  14. if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
  15. return Result.fail("秒杀已经结束");
  16. }
  17. //4.判断库存是否充足
  18. if (seckillVoucher.getStock() < 1) {
  19. return Result.fail("库存不足");
  20. }
  21. //5.一人一单
  22. Long userId = UserHolder.getUser().getId();
  23. //5.1判断是否存在订单
  24. Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  25. //5.2判断是否存在
  26. if (count > 0) {
  27. //用户已经购买过
  28. return Result.fail("用户已经购买一次");
  29. }
  30. //6.扣减库存
  31. boolean success = seckillVoucherService.update()
  32. .setSql("stock = stock - 1")//set stock = stock - 1
  33. .eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
  34. .update();
  35. if (!success) {
  36. return Result.fail("库存不足");
  37. }
  38. //7.创建订单
  39. VoucherOrder voucherOrder = new VoucherOrder();
  40. //7.1订单id
  41. voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
  42. //7.2用户id
  43. Long id = UserHolder.getUser().getId();
  44. voucherOrder.setUserId(id);
  45. //7.3代金券id
  46. voucherOrder.setVoucherId(voucherId);
  47. save(voucherOrder);
  48. //8.返回订单id
  49. return Result.ok();
  50. }
  51. }

 

 200个订单,为什么异常95%(为什么有10个订单)?

原因:多个线程穿插,并发情况(插入操作,不能用乐观锁),只能用悲观锁

  1. @Service
  2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  3. @Resource
  4. private ISeckillVoucherService seckillVoucherService;
  5. @Override
  6. public Result seckillVoucher(Long voucherId) {
  7. //1.查询优惠券信息
  8. SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
  9. //2.判断秒杀是否开始
  10. if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
  11. return Result.fail("秒杀尚未开始");
  12. }
  13. //3.判断秒杀是否结束
  14. if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
  15. return Result.fail("秒杀已经结束");
  16. }
  17. //4.判断库存是否充足
  18. if (seckillVoucher.getStock() < 1) {
  19. return Result.fail("库存不足");
  20. }
  21. Long userId = UserHolder.getUser().getId();
  22. synchronized (userId.toString().intern()) {//锁代码块需要共用同一把锁,所以做字符串转成同一对象
  23. //获取代理对象
  24. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
  25. return proxy.createVoucherOrder(voucherId);
  26. }
  27. }
  28. //在这个方法上加synchronized,它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁
  29. // ,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能
  30. @Transactional
  31. public Result createVoucherOrder(Long voucherId) {
  32. //5.一人一单
  33. Long userId = UserHolder.getUser().getId();
  34. //5.1判断是否存在订单
  35. Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  36. //5.2判断是否存在
  37. if (count > 0) {
  38. //用户已经购买过
  39. return Result.fail("用户已经购买一次");
  40. }
  41. //6.扣减库存
  42. boolean success = seckillVoucherService.update()
  43. .setSql("stock = stock - 1")//set stock = stock - 1
  44. .eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
  45. .update();
  46. if (!success) {
  47. return Result.fail("库存不足");
  48. }
  49. //7.创建订单
  50. VoucherOrder voucherOrder = new VoucherOrder();
  51. //7.1订单id
  52. voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
  53. //7.2用户id
  54. Long id = UserHolder.getUser().getId();
  55. voucherOrder.setUserId(id);
  56. //7.3代金券id
  57. voucherOrder.setVoucherId(voucherId);
  58. save(voucherOrder);
  59. //8.返回订单id
  60. return Result.ok();
  61. }
  62. }

总结:

1、synchronized锁不要加在createVoucherOrder方法上,虽然满足要求,但会影响性能。因为它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能。

2、synchronized代码块不要写在createVoucherOrder方法内部,虽然满足要求,但也会造成并发问题。因为方法上加了@Transactional,在内部加锁会造成事务没有提交就有订单,也会造成并发问题。

3、写synchronized代码块时,括号里面参数要唯一。因为锁代码块需要共用同一把锁,所以做字符串转成同一对象。

4、要调用有@Transactional的方法时,必须要用代理对象调用。因为@Transactional底层是代理对象,而一般调用方法是this调用。

集群模式下的并发安全问题

原因:多个jvm没有使用同一把锁

 解决方法:分布式锁,Redis锁,Redssion

 

总结:

1、Redis分布式锁可以用命令SETNX lock thread1和命令expire lock 10,但要保证是一个事务,所以用命令SET lock thread1 NX EX 10。

2、Redis分布式锁要设置过期时间,防止服务器宕机后死锁,且锁是非阻塞式,保证死锁后线程不会一直等待释放锁。

 1、tryLock和unLock工具类方法

  1. public class SimpleRedisLock {
  2. private StringRedisTemplate stringRedisTemplate;
  3. private String name;
  4. private static final String key_prefix = "lock:";
  5. public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
  6. this.stringRedisTemplate = stringRedisTemplate;
  7. this.name = name;
  8. }
  9. public boolean tryLock(long timeoutSec){
  10. long value = Thread.currentThread().getId();
  11. Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value + "", timeoutSec, TimeUnit.SECONDS);
  12. //自动拆箱,可能有安全问题,万一是null
  13. return Boolean.TRUE.equals(success);
  14. }
  15. public void unlock(){
  16. stringRedisTemplate.delete(key_prefix + name);
  17. }
  18. }

2、Redis分布式锁实现(部分逻辑)

  1. Long userId = UserHolder.getUser().getId();
  2. //创建锁对象
  3. SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
  4. //获取锁
  5. boolean isLock = lock.tryLock(1200);
  6. //判断是否获取锁成功
  7. if (!isLock) {
  8. return Result.fail("不允许重复下单");
  9. }
  10. //获取代理对象
  11. try {
  12. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
  13. return proxy.createVoucherOrder(voucherId);
  14. } finally {
  15. //释放锁
  16. lock.unlock();
  17. }

1、Redis分布式锁误删问题

产生原因:业务执行时间比锁时间长

线程一尝试获取锁,因为第一个来,所以获取锁成功 。因为某种原因业务产生阻塞,超过超时时间,线程一锁被释放。

线程二尝试获取锁成功,执行业务。线程一业务完成,释放锁(del),线程二锁被释放。

线程三尝试获取锁成功,执行业务。

结果:出现并发问题。

解决方法 :判断是否是自己的锁

  1. public class SimpleRedisLock {
  2. @Autowired
  3. private StringRedisTemplate stringRedisTemplate;
  4. private String name;
  5. private static final String key_prefix = "lock:";
  6. private static final String id_prefix = UUID.randomUUID().toString(true) + "-";
  7. public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
  8. this.stringRedisTemplate = stringRedisTemplate;
  9. this.name = name;
  10. }
  11. public boolean tryLock(long timeoutSec){
  12. //获取线程标识
  13. String value = id_prefix + Thread.currentThread().getId();
  14. Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value, timeoutSec, TimeUnit.SECONDS);
  15. //自动拆箱,可能有安全问题,万一是null
  16. return Boolean.TRUE.equals(success);
  17. }
  18. public void unlock(){
  19. //获取线程标识
  20. String value = id_prefix + Thread.currentThread().getId();
  21. //获取锁中标识
  22. String id = stringRedisTemplate.opsForValue().get(key_prefix + name);
  23. //判断是否一致
  24. if (id.equals(value)) {
  25. //释放锁
  26. stringRedisTemplate.delete(key_prefix + name);
  27. }
  28. }
  29. }

总结:在获取锁可以用UUID + 线程号表示,防止集群中出现相同标识。

 2、Redis锁的原子性问题

产生原因:当将要释放锁时产生阻塞。例如:jvm垃圾回收调用gc时会产生阻塞。

解决方法: Lua脚本

 

 

1、在Resource里编写Lua脚本 

  1. --比较线程标识和锁中的标识是否一致
  2. if (redis.call('get', KEYS[1]) == ARGV[1]) then
  3. --释放锁
  4. return redis.call('del',KEYS[1])
  5. end
  6. return 0

 2、调用Lua脚本

  1. public class SimpleRedisLock {
  2. @Autowired
  3. private StringRedisTemplate stringRedisTemplate;
  4. private String name;
  5. private static final String key_prefix = "lock:";
  6. private static final String id_prefix = UUID.randomUUID().toString(true) + "-";
  7. //读取Lua脚本
  8. private static final DefaultRedisScript<Long> unlock_script;
  9. static {
  10. unlock_script = new DefaultRedisScript<>();
  11. unlock_script.setLocation(new ClassPathResource("unlock.lua"));
  12. unlock_script.setResultType(Long.class);
  13. }
  14. public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
  15. this.stringRedisTemplate = stringRedisTemplate;
  16. this.name = name;
  17. }
  18. public boolean tryLock(long timeoutSec){
  19. //获取线程标识
  20. String value = id_prefix + Thread.currentThread().getId();
  21. Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value, timeoutSec, TimeUnit.SECONDS);
  22. //自动拆箱,可能有安全问题,万一是null
  23. return Boolean.TRUE.equals(success);
  24. }
  25. public void unlock(){
  26. //调用Lua脚本
  27. stringRedisTemplate.execute(unlock_script,
  28. Collections.singletonList(key_prefix + name),
  29. id_prefix + Thread.currentThread().getId());
  30. }
  31. }

Redission锁(不用自己写setnx,底层已经封装)

 1、引入redission依赖

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>2.9.0</version>
  5. </dependency>

2、配置redission 

  1. @Configuration
  2. public class RedissionConfig {
  3. public RedissonClient redissonClient(){
  4. //配置
  5. Config config = new Config();
  6. //useSingleServer()单节点模式
  7. config.useSingleServer().setAddress("redis://127.0.0.1:6379");//.setPassword("")
  8. //创建RedissonClient对象
  9. return Redisson.create(config);
  10. }
  11. }

3、代码

  1. @Service
  2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  3. @Autowired
  4. private StringRedisTemplate stringRedisTemplate;
  5. @Resource
  6. private ISeckillVoucherService seckillVoucherService;
  7. @Resource
  8. private RedissonClient redissonClient;
  9. @Override
  10. public Result seckillVoucher(Long voucherId) {
  11. //1.查询优惠券信息
  12. SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
  13. //2.判断秒杀是否开始
  14. if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
  15. return Result.fail("秒杀尚未开始");
  16. }
  17. //3.判断秒杀是否结束
  18. if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
  19. return Result.fail("秒杀已经结束");
  20. }
  21. //4.判断库存是否充足
  22. if (seckillVoucher.getStock() < 1) {
  23. return Result.fail("库存不足");
  24. }
  25. Long userId = UserHolder.getUser().getId();
  26. //创建锁对象
  27. // SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
  28. RLock lock = redissonClient.getLock("order" + userId);
  29. //获取锁
  30. // boolean isLock = lock.tryLock(1200);
  31. boolean isLock = lock.tryLock();
  32. //判断是否获取锁成功
  33. if (!isLock) {
  34. return Result.fail("不允许重复下单");
  35. }
  36. //获取代理对象
  37. try {
  38. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
  39. return proxy.createVoucherOrder(voucherId);
  40. } finally {
  41. //释放锁
  42. lock.unlock();
  43. }
  44. }
  45. //在这个方法上加synchronized,它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁
  46. // ,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能
  47. @Transactional
  48. public Result createVoucherOrder(Long voucherId) {
  49. //5.一人一单
  50. Long userId = UserHolder.getUser().getId();
  51. //5.1判断是否存在订单
  52. Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  53. //5.2判断是否存在
  54. if (count > 0) {
  55. //用户已经购买过
  56. return Result.fail("用户已经购买一次");
  57. }
  58. //6.扣减库存
  59. boolean success = seckillVoucherService.update()
  60. .setSql("stock = stock - 1")//set stock = stock - 1
  61. .eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
  62. .update();
  63. if (!success) {
  64. return Result.fail("库存不足");
  65. }
  66. //7.创建订单
  67. VoucherOrder voucherOrder = new VoucherOrder();
  68. //7.1订单id
  69. voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
  70. //7.2用户id
  71. Long id = UserHolder.getUser().getId();
  72. voucherOrder.setUserId(id);
  73. //7.3代金券id
  74. voucherOrder.setVoucherId(voucherId);
  75. save(voucherOrder);
  76. //8.返回订单id
  77. return Result.ok();
  78. }
  79. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/84215
推荐阅读
相关标签
  

闽ICP备14008679号