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

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分布式锁升级版RedLock及SpringBoot实现方法

    Redis分布式锁升级版RedLock及SpringBoot实现方法

    这篇文章主要介绍了Redis分布式锁升级版RedLock及SpringBoot实现,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以...

    等不到的口琴7802021-07-25
  • Redisredis启动,停止,及端口占用处理方法

    redis启动,停止,及端口占用处理方法

    今天小编就为大家分享一篇redis启动,停止,及端口占用处理方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    澄海单挑狂5152019-11-14
  • Redis在ssm项目中使用redis缓存查询数据的方法

    在ssm项目中使用redis缓存查询数据的方法

    本文主要简单的使用Java代码进行redis缓存,即在查询的时候先在service层从redis缓存中获取数据。如果大家对在ssm项目中使用redis缓存查询数据的相关知识感...

    caychen8962019-11-12
  • Redis聊一聊Redis与MySQL双写一致性如何保证

    聊一聊Redis与MySQL双写一致性如何保证

    一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。本文给大家分享Redis与MySQL双写一致性该如何保证,感兴趣的朋友一...

    mind_programmonkey6432021-08-12
  • RedisRedis数据结构之链表与字典的使用

    Redis数据结构之链表与字典的使用

    这篇文章主要介绍了Redis数据结构之链表与字典的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    白泽来了4052021-08-03
  • Redis就这?Redis持久化策略——AOF

    就这?Redis持久化策略——AOF

    今天为大家介绍Redis的另一种持久化策略——AOF。注意:AOF文件只会记录Redis的写操作命令,因为读命令对数据的恢复没有任何意义...

    头发茂密的刘叔4052021-12-14
  • RedisRedis存取序列化与反序列化性能问题详解

    Redis存取序列化与反序列化性能问题详解

    这篇文章主要给大家介绍了关于Redis存取序列化与反序列化性能问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参...

    这名字已经存在9742021-02-24
  • RedisLinux Redis 的安装步骤详解

    Linux Redis 的安装步骤详解

    这篇文章主要介绍了 Linux Redis 的安装步骤详解的相关资料,希望大家通过本文能掌握如何安装Redis,需要的朋友可以参考下 ...

    carl-zhao3822019-11-08