Redis配合Lua实现高并发防止秒杀超卖,实战源码解决方案

独行侠梦

于 2021-11-17 23:00:00 发布

阅读量1.7k

 收藏 4

点赞数 1

文章标签: 数据库 java redis mysql 分布式

版权

◆ 1:解决思路

将活动写入 redis 中,通过 redis 自减指令扣除库存。

◆ 2:添加 redis 常量

commons/constant/RedisKeyConstant.java 

seckill_vouchers("seckill_vouchers:","秒杀券的 key"),

◆ 3:添加 redis 配置类

b537e2a71c214ed2c5cefca2ee044682.png

◆ 4:修改业务层

废话不多说,直接上源码

1:秒杀业务逻辑层

 
  1. @Service

  2. public class SeckillService {

  3. @Resource

  4. private SeckillVouchersMapper seckillVouchersMapper;

  5. @Resource

  6. 2private VoucherOrdersMapper voucherOrdersMapper;

  7. @Value("${service.name.ms-oauth-server}")

  8. private String oauthServerName;

  9. @Resource

  10. private RestTemplate restTemplate;

  11. @Resource

  12. private RedisTemplate redisTemplate;

2:添加需要抢购的代金券

 
  1. @Transactional(rollbackFor = Exception.class)

  2. public void addSeckillVouchers(SeckillVouchers seckillVouchers) {

  3. // 非空校验

  4. AssertUtil.isTrue(seckillVouchers.getFkVoucherId()== null,"请选择需要抢购的代金券");

  5. AssertUtil.isTrue(seckillVouchers.getAmount()== 0,"请输入抢购总数量");

  6. Date now = new Date();

  7. AssertUtil.isNotNull(seckillVouchers.getStartTime(),"请输入开始时间");

  8. // 生产环境下面一行代码需放行,这里注释方便测试

  9. // AssertUtil.isTrue(now.after(seckillVouchers.getStartTime()),"开始时间不能早于当前时间");

  10. AssertUtil.isNotNull(seckillVouchers.getEndTime(),"请输入结束时间");

  11. AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()),"结束时间不能早于当前时间");

  12. AssertUtil.isTrue(seckillVouchers.getStartTime().after(seckillVouchers.getEndTime()),"开始时间不能晚于结束时间");

  13. // 采用 Redis 实现

  14. String key= RedisKeyConstant.seckill_vouchers.getKey() +seckillVouchers.getFkVoucherId();

  15. // 验证 Redis 中是否已经存在该券的秒杀活动,hash 不会做序列化和反序列化,

  16. 有利于性能的提高。entries(key),取到 key

  17. Map<String, Object> map= redisTemplate.opsForHash().entries(key);

  18. //如果不为空或 amount 库存>0,该券已经拥有了抢购活动,就不要再创建。

  19. AssertUtil.isTrue(!map.isEmpty() && (int) map.get("amount") > 0,"该券已经拥有了抢购活动");

  20. // 抢购活动数据插入 Redis

  21. seckillVouchers.setIsValid(1);

  22. seckillVouchers.setCreateDate(now);

  23. seckillVouchers.setUpdateDate(now);

  24. //key 对应的是 map,使用工具集将 seckillVouchers 转成 map

  25. redisTemplate.opsForHash().putAll(key,BeanUtil.beanToMap(seckillVouchers));

  26. }

3:抢购代金券

 
  1. @Transactional(rollbackFor = Exception.class)

  2. public ResultInfo doSeckill(Integer voucherId, String accessToken, String path)

  3. {

  4. // 基本参数校验

  5. AssertUtil.isTrue(voucherId == null || voucherId < 0,"请选择需要抢购的代金券");

  6. AssertUtil.isNotEmpty(accessToken,"请登录");

  7. // 采用 Redis

  8. String key= RedisKeyConstant.seckill_vouchers.getKey() + voucherId;//根据 key 获取 map

  9. Map<String, Object> map= redisTemplate.opsForHash().entries(key);

  10. //map 转对象

  11. SeckillVouchers seckillVouchers = BeanUtil.mapToBean(map,SeckillVouchers.class, true, null);

  12. // 判断是否开始、结束

  13. Date now = new Date();

  14. AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()),"该抢购还未开始");

  15. AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()),"该抢购已结束");

  16. // 判断是否卖完

  17. AssertUtil.isTrue(seckillVouchers.getAmount() < 1,"该券已经卖完了");

  18. // 获取登录用户信息

  19. String url = oauthServerName +"user/me?access_token={accessToken}";

  20. ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class,accessToken);

  21. if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {

  22. resultInfo.setPath(path);

  23. return resultInfo;

  24. }

  25. // 这里的 data 是一个 LinkedHashMap,SignInDinerInfo

  26. SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap)resultInfo.getData(), new SignInDinerInfo(), false);

  27. // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)

  28. VoucherOrders order =voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),seckillVouchers.getFkVoucherId());

  29. AssertUtil.isTrue(order != null,"该用户已抢到该代金券,无需再抢");

  30. //扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1

  31. long count = redisTemplate.opsForHash().increment(key,"amount",-1);

  32. AssertUtil.isTrue(count < 0,"该券已经卖完了");

  33. // 下单存储到数据库

  34. VoucherOrders voucherOrders = new VoucherOrders();

  35. voucherOrders.setFkDinerId(dinerInfo.getId());

  36. // Redis 中不需要维护外键信息

  37. //voucherOrders.setFkSeckillId(seckillVouchers.getId());

  38. voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());

  39. String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();

  40. voucherOrders.setOrderNo(orderNo);

  41. voucherOrders.setOrderType(1);

  42. voucherOrders.setStatus(0);

  43. count = voucherOrdersMapper.save(voucherOrders);

  44. AssertUtil.isTrue(count == 0,"用户抢购失败");

  45. return ResultInfoUtil.buildSuccess(path,"抢购成功");

  46. }

  47. }

◆ 5:postman 测试

http://localhost:8083/add

 
  1. {

  2. "fkVoucherId":1,

  3. "amount":100,

  4. "startTime":"2020-02-04 11:12:00",

  5. "endTime":"2021-02-06 11:12:00"

  6. }

7427619a64de756bfd586fb46a6cfb3c.png

 查看 redis

fa8189adb43cee71c5a38cd663cc88c6.png

 再次运行 http://localhost:8083/add

680f6f359a9dca53ef3c9700015fc9b5.png

◆  6:压力测试

a2f4b4a9d4e2b24a4575a773d2f1def3.png

 查看 redis 中的库存出现负值

9c4c67b4168adc20c34f8fba2fbfacb4.png

在 redis 中修改库存要分两部进行,先要获取库存的值,再扣减库存。所以在高并

发情况下,会导致 redis 扣减库存出问题。可以使用 redis 的弱事务或 lua 脚本解决。

◆ 7:安装Lua

resources/stock.lua

 
  1. if (redis.call('hexists', KEYS[1], KEYS[2])== 1) then

  2. local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));

  3. if (stock > 0) then

  4. redis.call('hincrby', KEYS[1], KEYS[2],-1);

  5. return stock;

  6. end;

  7. return 0;

  8. end;

hexists', KEYS[1], KEYS[2]) == 1

hexists 是判断 redis 中 key 是否存在。

KEYS[1] 是 seckill_vouchers:1 KEYS[2] 是 amount

hget 是获取 amount 赋给 stock

hincrby 是自增,当为-1 是为自减。

因为在 redis 中没有自减指令,所以当步长为 -1 表示自减。

现在使用 lua 脚本,将 redis 中查询库存和扣减库存当成原子性操作在一个线程内.

◆ 8:配置Lua

config/RedisTemplateConfiguration.java

 
  1. @Bean

  2. public DefaultRedisScript<Long> stockScript() {

  3. DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

  4. //放在和 application.yml 同层目录下

  5. redisScript.setLocation(new ClassPathResource("stock.lua"));

  6. redisScript.setResultType(Long.class);

  7. return redisScript;

  8. }

◆ 9:修改业务层

ms-seckill/service/SeckilService.java

1:抢购代金券

 
  1. @Transactional(rollbackFor = Exception.class)

  2. public ResultInfo doSeckill(Integer voucherId, String accessToken, String path)

  3. {

  4. // 基本参数校验

  5. AssertUtil.isTrue(voucherId == null || voucherId < 0,"请选择需要抢购的代金券");

  6. AssertUtil.isNotEmpty(accessToken,"请登录");

  7. // 采用 Redis

  8. String key= RedisKeyConstant.seckill_vouchers.getKey() + voucherId;

  9. //根据 key 获取 map

  10. Map<String, Object> map= redisTemplate.opsForHash().entries(key);

  11. //map 转对象

  12. SeckillVouchers seckillVouchers = BeanUtil.mapToBean(map,SeckillVouchers.class, true, null);

  13. // 判断是否开始、结束

  14. Date now = new Date();AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()),"该抢购还未开始");

  15. AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()),"该抢购已结束");

  16. // 判断是否卖完

  17. AssertUtil.isTrue(seckillVouchers.getAmount() < 1,"该券已经卖完了");

  18. // 获取登录用户信息

  19. String url = oauthServerName +"user/me?access_token={accessToken}";

  20. ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class,

  21. accessToken);

  22. if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {

  23. resultInfo.setPath(path);

  24. return resultInfo;

  25. }

  26. // 这里的 data 是一个 LinkedHashMap,SignInDinerInfo

  27. SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap)

  28. resultInfo.getData(), new SignInDinerInfo(), false);

  29. // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)

  30. VoucherOrders order =voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),

  31. seckillVouchers.getFkVoucherId());

  32. AssertUtil.isTrue(order != null,"该用户已抢到该代金券,无需再抢");

  33. //扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1

  34. // long count = redisTemplate.opsForHash().increment(key,"amount",-1);

  35. // AssertUtil.isTrue(count < 0,"该券已经卖完了");

  36. // 下单存储到数据库

  37. VoucherOrders voucherOrders = new VoucherOrders();

  38. voucherOrders.setFkDinerId(dinerInfo.getId());

  39. // Redis 中不需要维护外键信息

  40. //voucherOrders.setFkSeckillId(seckillVouchers.getId());

  41. voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());

  42. String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();

  43. voucherOrders.setOrderNo(orderNo);

  44. voucherOrders.setOrderType(1);

  45. voucherOrders.setStatus(0);

  46. long count = voucherOrdersMapper.save(voucherOrders);

  47. AssertUtil.isTrue(count == 0,"用户抢购失败");

  48. // 采用 Redis + Lua 解决问题

  49. // 扣库存

  50. List<String> keys = new ArrayList<>();

  51. //将 redis 的 key 放进去keys.add(key);

  52. keys.add("amount");

  53. Long amount =(Long) redisTemplate.execute(defaultRedisScript, keys);

  54. AssertUtil.isTrue(amount == null || amount < 1,"该券已经卖完了");

  55. return ResultInfoUtil.buildSuccess(path,"抢购成功");

  56. }

◆ 10:压力测试

将 redis 中库存改回 100

bd15ab05e16f7cb60a83c8d0a18998ee.png

压力测试

bbd1964ef7e291e82cda602f659e1542.png

查看 redis 中 amount=0 ,不会变成负值

查看数据库下单表 t_voucher_orders ,共计下 100 个订单。

 73ea9bc1433d7ea233662519250863f3.png

往期推荐

天秀!搞java的技术人写了本小说:《JavaScript百炼成仙》

55 个细节帮你全方位的完成Java 性能优化的 (珍藏)

Java 实现视频弹幕功能

Java的ClassLoader加载是怎么保证安全的?

Redis分布式锁需要考虑的这些事!

Redis 面试题!精华!收藏一波 !

解决kafka 消息堆积问题的排查及调优

MySQL 的隔离级别和事务需要知道的

2c6a7c99ea70ef6c7776f06124ccc484.gif

回复干货】获取精选干货视频教程

回复加群】加入疑难问题攻坚交流群

回复mat】获取内存溢出问题分析详细文档教程

回复赚钱】获取用java写一个能赚钱的微信机器人

回复副业】获取程序员副业攻略一份

79a5ec85402499fa4a8a75e76daf9a7c.png

86236fd6e9abcfb26036a16f123e735f.gif

戳这儿

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值