目录
- 1:解决思路
- 2:添加 redis 常量
- 3:添加 redis 配置类
- 4:修改业务层
- 1:秒杀业务逻辑层
- 2:添加需要抢购的代金券
- 3:抢购代金券
- 5:postman 测试
- 6:压力测试
- 8:配置lua
- 9:修改业务层
- 1:抢购代金券
- 10:压力测试
1:解决思路
将活动写入 redis 中,通过 redis 自减指令扣除库存。
2:添加 redis 常量
commons/constant/rediskeyconstant.java
seckill_vouchers("seckill_vouchers:","秒杀券的 key"),
3:添加 redis 配置类
4:修改业务层
废话不多说,直接上源码
1:秒杀业务逻辑层
@service public class seckillservice { @resource private seckillvouchersmapper seckillvouchersmapper; @resource 2private voucherordersmapper voucherordersmapper; @value("${service.name.ms-oauth-server}") private string oauthservername; @resource private resttemplate resttemplate; @resource private redistemplate redistemplate;
2:添加需要抢购的代金券
@transactional(rollbackfor = exception.class) public void addseckillvouchers(seckillvouchers seckillvouchers) { // 非空校验 assertutil.istrue(seckillvouchers.getfkvoucherid()== null,"请选择需要抢购的代金券"); assertutil.istrue(seckillvouchers.getamount()== 0,"请输入抢购总数量"); date now = new date(); assertutil.isnotnull(seckillvouchers.getstarttime(),"请输入开始时间"); // 生产环境下面一行代码需放行,这里注释方便测试 // assertutil.istrue(now.after(seckillvouchers.getstarttime()),"开始时间不能早于当前时间"); assertutil.isnotnull(seckillvouchers.getendtime(),"请输入结束时间"); assertutil.istrue(now.after(seckillvouchers.getendtime()),"结束时间不能早于当前时间"); assertutil.istrue(seckillvouchers.getstarttime().after(seckillvouchers.getendtime()),"开始时间不能晚于结束时间"); // 采用 redis 实现 string key= rediskeyconstant.seckill_vouchers.getkey() +seckillvouchers.getfkvoucherid(); // 验证 redis 中是否已经存在该券的秒杀活动,hash 不会做序列化和反序列化, 有利于性能的提高。entries(key),取到 key map<string, object> map= redistemplate.opsforhash().entries(key); //如果不为空或 amount 库存>0,该券已经拥有了抢购活动,就不要再创建。 assertutil.istrue(!map.isempty() && (int) map.get("amount") > 0,"该券已经拥有了抢购活动"); // 抢购活动数据插入 redis seckillvouchers.setisvalid(1); seckillvouchers.setcreatedate(now); seckillvouchers.setupdatedate(now); //key 对应的是 map,使用工具集将 seckillvouchers 转成 map redistemplate.opsforhash().putall(key,beanutil.beantomap(seckillvouchers)); }
3:抢购代金券
@transactional(rollbackfor = exception.class) public resultinfo doseckill(integer voucherid, string accesstoken, string path) { // 基本参数校验 assertutil.istrue(voucherid == null || voucherid < 0,"请选择需要抢购的代金券"); assertutil.isnotempty(accesstoken,"请登录"); // 采用 redis string key= rediskeyconstant.seckill_vouchers.getkey() + voucherid;//根据 key 获取 map map<string, object> map= redistemplate.opsforhash().entries(key); //map 转对象 seckillvouchers seckillvouchers = beanutil.maptobean(map,seckillvouchers.class, true, null); // 判断是否开始、结束 date now = new date(); assertutil.istrue(now.before(seckillvouchers.getstarttime()),"该抢购还未开始"); assertutil.istrue(now.after(seckillvouchers.getendtime()),"该抢购已结束"); // 判断是否卖完 assertutil.istrue(seckillvouchers.getamount() < 1,"该券已经卖完了"); // 获取登录用户信息 string url = oauthservername +"user/me?access_token={accesstoken}"; resultinfo resultinfo = resttemplate.getforobject(url, resultinfo.class,accesstoken); if (resultinfo.getcode() != apiconstant.success_code) { resultinfo.setpath(path); return resultinfo; } // 这里的 data 是一个 linkedhashmap,signindinerinfo signindinerinfo dinerinfo = beanutil.fillbeanwithmap((linkedhashmap)resultinfo.getdata(), new signindinerinfo(), false); // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次) voucherorders order =voucherordersmapper.finddinerorder(dinerinfo.getid(),seckillvouchers.getfkvoucherid()); assertutil.istrue(order != null,"该用户已抢到该代金券,无需再抢"); //扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1 long count = redistemplate.opsforhash().increment(key,"amount",-1); assertutil.istrue(count < 0,"该券已经卖完了"); // 下单存储到数据库 voucherorders voucherorders = new voucherorders(); voucherorders.setfkdinerid(dinerinfo.getid()); // redis 中不需要维护外键信息 //voucherorders.setfkseckillid(seckillvouchers.getid()); voucherorders.setfkvoucherid(seckillvouchers.getfkvoucherid()); string orderno = idutil.getsnowflake(1, 1).nextidstr(); voucherorders.setorderno(orderno); voucherorders.setordertype(1); voucherorders.setstatus(0); count = voucherordersmapper.save(voucherorders); assertutil.istrue(count == 0,"用户抢购失败"); return resultinfoutil.buildsuccess(path,"抢购成功"); } }
5:postman 测试
http://localhost:8083/add
{ "fkvoucherid":1, "amount":100, "starttime":"2020-02-04 11:12:00", "endtime":"2021-02-06 11:12:00" }
查看 redis
再次运行 http://localhost:8083/add
6:压力测试
查看 redis 中的库存出现负值
在 redis 中修改库存要分两部进行,先要获取库存的值,再扣减库存。所以在高并 发情况下,会导致 redis 扣减库存出问题。可以使用 redis 的弱事务或 lua 脚本解决。 7:安装lua resources/stock.lua
if (redis.call('hexists', keys[1], keys[2])== 1) then local stock = tonumber(redis.call('hget', keys[1], keys[2])); if (stock > 0) then redis.call('hincrby', keys[1], keys[2],-1); return stock; end; return 0; 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
@bean public defaultredisscript<long> stockscript() { defaultredisscript<long> redisscript = new defaultredisscript<>(); //放在和 application.yml 同层目录下 redisscript.setlocation(new classpathresource("stock.lua")); redisscript.setresulttype(long.class); return redisscript; }
9:修改业务层
ms-seckill/service/seckilservice.java
1:抢购代金券
@transactional(rollbackfor = exception.class) public resultinfo doseckill(integer voucherid, string accesstoken, string path) { // 基本参数校验 assertutil.istrue(voucherid == null || voucherid < 0,"请选择需要抢购的代金券"); assertutil.isnotempty(accesstoken,"请登录"); // 采用 redis string key= rediskeyconstant.seckill_vouchers.getkey() + voucherid; //根据 key 获取 map map<string, object> map= redistemplate.opsforhash().entries(key); //map 转对象 seckillvouchers seckillvouchers = beanutil.maptobean(map,seckillvouchers.class, true, null); // 判断是否开始、结束 date now = new date();assertutil.istrue(now.before(seckillvouchers.getstarttime()),"该抢购还未开始"); assertutil.istrue(now.after(seckillvouchers.getendtime()),"该抢购已结束"); // 判断是否卖完 assertutil.istrue(seckillvouchers.getamount() < 1,"该券已经卖完了"); // 获取登录用户信息 string url = oauthservername +"user/me?access_token={accesstoken}"; resultinfo resultinfo = resttemplate.getforobject(url, resultinfo.class, accesstoken); if (resultinfo.getcode() != apiconstant.success_code) { resultinfo.setpath(path); return resultinfo; } // 这里的 data 是一个 linkedhashmap,signindinerinfo signindinerinfo dinerinfo = beanutil.fillbeanwithmap((linkedhashmap) resultinfo.getdata(), new signindinerinfo(), false); // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次) voucherorders order =voucherordersmapper.finddinerorder(dinerinfo.getid(), seckillvouchers.getfkvoucherid()); assertutil.istrue(order != null,"该用户已抢到该代金券,无需再抢"); //扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1 // long count = redistemplate.opsforhash().increment(key,"amount",-1); // assertutil.istrue(count < 0,"该券已经卖完了"); // 下单存储到数据库 voucherorders voucherorders = new voucherorders(); voucherorders.setfkdinerid(dinerinfo.getid()); // redis 中不需要维护外键信息 //voucherorders.setfkseckillid(seckillvouchers.getid()); voucherorders.setfkvoucherid(seckillvouchers.getfkvoucherid()); string orderno = idutil.getsnowflake(1, 1).nextidstr(); voucherorders.setorderno(orderno); voucherorders.setordertype(1); voucherorders.setstatus(0); long count = voucherordersmapper.save(voucherorders); assertutil.istrue(count == 0,"用户抢购失败"); // 采用 redis + lua 解决问题 // 扣库存 list<string> keys = new arraylist<>(); //将 redis 的 key 放进去keys.add(key); keys.add("amount"); long amount =(long) redistemplate.execute(defaultredisscript, keys); assertutil.istrue(amount == null || amount < 1,"该券已经卖完了"); return resultinfoutil.buildsuccess(path,"抢购成功"); }
10:压力测试
将 redis 中库存改回 100
压力测试
查看 redis 中 amount=0 ,不会变成负值 查看数据库下单表 t_voucher_orders ,共计下 100 个订单。
到此这篇关于redis高并发防止秒杀超卖实战源码解决方案的文章就介绍到这了,更多相关redis高并发防止秒杀超卖 内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!