服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - Redis高并发防止秒杀超卖实战源码解决方案

Redis高并发防止秒杀超卖实战源码解决方案

2021-11-22 18:17不要迷恋发哥 Redis

本文主要介绍了Redis高并发防止秒杀超卖实战源码解决方案,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

1:解决思路

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

2:添加 redis 常量

commons/constant/rediskeyconstant.java

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

3:添加 redis 配置类

Redis高并发防止秒杀超卖实战源码解决方案

4:修改业务层

废话不多说,直接上源码

1:秒杀业务逻辑层

?
1
2
3
4
5
6
7
8
9
10
11
12
@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:添加需要抢购的代金券

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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:抢购代金券

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@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

?
1
2
3
4
5
6
{
"fkvoucherid":1,
"amount":100,
"starttime":"2020-02-04 11:12:00",
"endtime":"2021-02-06 11:12:00"
}

Redis高并发防止秒杀超卖实战源码解决方案

查看 redis

Redis高并发防止秒杀超卖实战源码解决方案

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

Redis高并发防止秒杀超卖实战源码解决方案

6:压力测试

Redis高并发防止秒杀超卖实战源码解决方案

查看 redis 中的库存出现负值

Redis高并发防止秒杀超卖实战源码解决方案

在 redis 中修改库存要分两部进行,先要获取库存的值,再扣减库存。所以在高并 发情况下,会导致 redis 扣减库存出问题。可以使用 redis 的弱事务或 lua 脚本解决。 7:安装lua resources/stock.lua

?
1
2
3
4
5
6
7
8
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

?
1
2
3
4
5
6
7
8
@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:抢购代金券

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@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高并发防止秒杀超卖实战源码解决方案

压力测试

Redis高并发防止秒杀超卖实战源码解决方案

查看 redis 中 amount=0 ,不会变成负值 查看数据库下单表 t_voucher_orders ,共计下 100 个订单。

Redis高并发防止秒杀超卖实战源码解决方案

到此这篇关于redis高并发防止秒杀超卖实战源码解决方案的文章就介绍到这了,更多相关redis高并发防止秒杀超卖 内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/chongfa2008/article/details/120941962

延伸 · 阅读

精彩推荐
  • RedisRedis 事务知识点相关总结

    Redis 事务知识点相关总结

    这篇文章主要介绍了Redis 事务相关总结,帮助大家更好的理解和学习使用Redis,感兴趣的朋友可以了解下...

    AsiaYe8232021-07-28
  • Redisredis实现排行榜功能

    redis实现排行榜功能

    排行榜在很多地方都能使用到,redis的zset可以很方便地用来实现排行榜功能,本文就来简单的介绍一下如何使用,具有一定的参考价值,感兴趣的小伙伴们...

    乘月归5022021-08-05
  • Redis详解Redis复制原理

    详解Redis复制原理

    与大多数db一样,Redis也提供了复制机制,以满足故障恢复和负载均衡等需求。复制也是Redis高可用的基础,哨兵和集群都是建立在复制基础上实现高可用的...

    李留广10222021-08-09
  • RedisRedis全量复制与部分复制示例详解

    Redis全量复制与部分复制示例详解

    这篇文章主要给大家介绍了关于Redis全量复制与部分复制的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Redis爬虫具有一定的参考学习...

    豆子先生5052019-11-27
  • RedisRedis的配置、启动、操作和关闭方法

    Redis的配置、启动、操作和关闭方法

    今天小编就为大家分享一篇Redis的配置、启动、操作和关闭方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    大道化简5312019-11-14
  • Redisredis中如何使用lua脚本让你的灵活性提高5个逼格详解

    redis中如何使用lua脚本让你的灵活性提高5个逼格详解

    这篇文章主要给大家介绍了关于redis中如何使用lua脚本让你的灵活性提高5个逼格的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具...

    一线码农5812019-11-18
  • RedisRedis如何实现数据库读写分离详解

    Redis如何实现数据库读写分离详解

    Redis的主从架构,能帮助我们实现读多,写少的情况,下面这篇文章主要给大家介绍了关于Redis如何实现数据库读写分离的相关资料,文中通过示例代码介绍...

    罗兵漂流记6092019-11-11
  • Redisredis 交集、并集、差集的具体使用

    redis 交集、并集、差集的具体使用

    这篇文章主要介绍了redis 交集、并集、差集的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    xiaojin21cen10152021-07-27