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

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

延伸 · 阅读

精彩推荐
  • Redis如何使用Redis锁处理并发问题详解

    如何使用Redis锁处理并发问题详解

    这篇文章主要给大家介绍了关于如何使用Redis锁处理并发问题的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Redis具有一定的参考学习...

    haofly4522019-11-26
  • Redis关于Redis数据库入门详细介绍

    关于Redis数据库入门详细介绍

    大家好,本篇文章主要讲的是关于Redis数据库入门详细介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览...

    沃尔码6982022-01-24
  • RedisRedis Template实现分布式锁的实例代码

    Redis Template实现分布式锁的实例代码

    这篇文章主要介绍了Redis Template实现分布式锁,需要的朋友可以参考下 ...

    晴天小哥哥2592019-11-18
  • RedisRedis 6.X Cluster 集群搭建

    Redis 6.X Cluster 集群搭建

    码哥带大家完成在 CentOS 7 中安装 Redis 6.x 教程。在学习 Redis Cluster 集群之前,我们需要先搭建一套集群环境。机器有限,实现目标是一台机器上搭建 6 个节...

    码哥字节15752021-04-07
  • Redisredis缓存存储Session原理机制

    redis缓存存储Session原理机制

    这篇文章主要为大家介绍了redis缓存存储Session原理机制详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    程序媛张小妍9252021-11-25
  • Redis《面试八股文》之 Redis十六卷

    《面试八股文》之 Redis十六卷

    redis 作为我们最常用的内存数据库,很多地方你都能够发现它的身影,比如说登录信息的存储,分布式锁的使用,其经常被我们当做缓存去使用。...

    moon聊技术8182021-07-26
  • RedisRedis集群的5种使用方式,各自优缺点分析

    Redis集群的5种使用方式,各自优缺点分析

    Redis 多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。...

    优知学院4082021-08-10
  • Redis详解三分钟快速搭建分布式高可用的Redis集群

    详解三分钟快速搭建分布式高可用的Redis集群

    这篇文章主要介绍了详解三分钟快速搭建分布式高可用的Redis集群,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    万猫学社4502021-07-25