一、全域唯一ID
1. 全域ID產生器
每個店舖都可以發布優惠券:
當使用者搶購時,就會產生訂單並儲存到tb_voucher_order這張表中,而訂單表如果使用資料庫自增ID就存在一些問題:
-
#id的規律性太明顯
受單表資料量的限制
所以tb_voucher_order表的主鍵不能用自增ID:
create table tb_voucher_order ( id bigint not null comment '主键' primary key, user_id bigint unsigned not null comment '下单的用户id', voucher_id bigint unsigned not null comment '购买的代金券id', pay_type tinyint(1) unsigned default 1 not null comment '支付方式 1:余额支付;2:支付宝;3:微信', status tinyint(1) unsigned default 1 not null comment '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款', create_time timestamp default CURRENT_TIMESTAMP not null comment '下单时间', pay_time timestamp null comment '支付时间', use_time timestamp null comment '核销时间', refund_time timestamp null comment '退款时间', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' );
全域ID產生器,是分散式系統下用來產生全域唯一ID的工具,一般要滿足下列特性:
為了增加ID的安全性,我們可以不直接使用Redis自增的數值,而是拼接一些其它訊息:
D的組成部分:
符號位元:1bit,永遠為0,表示正數
時間戳:31bit,以秒為單位,可以使用69年
序號:32bit,秒內的計數器,支援每秒產生2^32個不同ID
##編寫全域ID產生器程式碼:
@Component public class RedisIdWorker { /** * 开始时间戳,以2022.1.1为基准计算时间差 */ private static final long BEGIN_TIMESTAMP = 1640995200L; /** * 序列号的位数 */ private static final int COUNT_BITS = 32; private StringRedisTemplate stringRedisTemplate; public RedisIdWorker(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 生成带有业务前缀的redis自增id * @param keyPrefix * @return */ public long nextId(String keyPrefix) { // 1.生成时间戳 LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSecond - BEGIN_TIMESTAMP; // 2.生成序列号 // 2.1.获取当前日期,精确到天 // 加上日期前缀,可以让存更多同一业务类型的数据,并且还能通过日期获取当天的业务数量,一举两得 String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); // 2.2.自增长 long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); // 3.拼接并返回 // 用于是数字类型的拼接,所以不能像拼接字符串那样处理,而是通过位运算将高32位存 符号位+时间戳,低32位存 序列号 return timestamp << COUNT_BITS | count; } public static void main(String[] args) { LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0); long second = time.toEpochSecond(ZoneOffset.UTC); System.out.println(second);// 1640995200 } }
測試全域ID產生器:
@SpringBootTest class HmDianPingApplicationTests { @Resource private RedisIdWorker redisIdWorker; private ExecutorService executorService = Executors.newFixedThreadPool(500); @Test void testIdWorker() throws InterruptedException { CountDownLatch latch = new CountDownLatch(300); // 每个线程生成100个id Runnable task = () -> { for (int i = 0; i < 100; i++) { long id = redisIdWorker.nextId("order"); System.out.println("id = " + id); } latch.countDown(); }; // 300个线程 long begin = System.currentTimeMillis(); for (int i = 0; i < 300; i++) { executorService.submit(task); } latch.await(); long end = System.currentTimeMillis(); System.out.println("time = " + (end - begin)); } }##測試結果:
2. 全域唯一ID產生策略
- UUID(不是遞增的)
- 雪花演算法(snowflake)
- 資料庫自增(單獨建立一張表存自增id,分配到分庫分錶後的表中)
- 3. Redis自增ID策略
- 以日期作為前綴的key ,方便統計訂單量
- 自增ID的結構:時間戳計數器
- 二、實作優惠券秒殺下單
1. 新增優惠券
每個店家都可以發布優惠券,分為平價券和特價券。平價券可以任意購買,而特價券需要秒殺搶購:
#優惠券表資訊:
- tb_seckill_voucher:優惠券的庫存、開始搶購時間,結束搶購時間(秒殺券才需要填寫這些資訊),同時秒殺券擁有普通券的基本資料(秒殺券表tb_seckill_voucher的主鍵id綁定的是普通券表tb_voucher的id)
-
#
create table tb_voucher ( id bigint unsigned auto_increment comment '主键' primary key, shop_id bigint unsigned null comment '商铺id', title varchar(255) not null comment '代金券标题', sub_title varchar(255) null comment '副标题', rules varchar(1024) null comment '使用规则', pay_value bigint(10) unsigned not null comment '支付金额,单位是分。例如200代表2元', actual_value bigint(10) not null comment '抵扣金额,单位是分。例如200代表2元', type tinyint(1) unsigned default 0 not null comment '0,普通券;1,秒杀券', status tinyint(1) unsigned default 1 not null comment '1,上架; 2,下架; 3,过期', create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' );
create table tb_seckill_voucher ( voucher_id bigint unsigned not null comment '关联的优惠券的id' primary key, stock int(8) not null comment '库存', create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间', begin_time timestamp default CURRENT_TIMESTAMP not null comment '生效时间', end_time timestamp default CURRENT_TIMESTAMP not null comment '失效时间', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' ) comment '秒杀优惠券表,与优惠券是一对一关系';
2. 編寫新增秒殺券的介面
主要程式碼:
@RestController @RequestMapping("/voucher") public class VoucherController { @Resource private IVoucherService voucherService; /** * 新增秒杀券 * @param voucher 优惠券信息,包含秒杀信息 * @return 优惠券id */ @PostMapping("seckill") public Result addSeckillVoucher(@RequestBody Voucher voucher) { voucherService.addSeckillVoucher(voucher); return Result.ok(voucher.getId()); } }
@Service public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService { @Resource private ISeckillVoucherService seckillVoucherService; @Override @Transactional public void addSeckillVoucher(Voucher voucher) { // 保存优惠券 save(voucher); // 保存秒杀信息 SeckillVoucher seckillVoucher = new SeckillVoucher(); seckillVoucher.setVoucherId(voucher.getId()); seckillVoucher.setStock(voucher.getStock()); seckillVoucher.setBeginTime(voucher.getBeginTime()); seckillVoucher.setEndTime(voucher.getEndTime()); seckillVoucherService.save(seckillVoucher); } }測試新增:
測試結果:
三、實作秒殺下單
下單時需要判斷兩點:
秒殺是否開始或結束,如果尚未開始或已經結束則無法下單
#主程式碼:
@RestController @RequestMapping("/voucher-order") public class VoucherOrderController { @Resource private IVoucherOrderService voucherOrderService; @PostMapping("seckill/{id}") public Result seckillVoucher(@PathVariable("id") Long voucherId) { return voucherOrderService.seckillVoucher(voucherId); } }
@Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // 2.判断秒杀是否开始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀尚未开始!"); } // 3.判断秒杀是否已经结束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀已经结束!"); } // 4.判断库存是否充足 if (voucher.getStock() < 1) { // 库存不足 return Result.fail("库存不足!"); } //5,扣减库存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).update(); if (!success) { // 扣减库存失败 return Result.fail("库存不足!"); } // 6.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); // 6.1.订单id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); // 6.2.用户id Long userId = UserHolder.getUser().getId(); voucherOrder.setUserId(userId); // 6.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); return Result.ok(orderId); } }
簡單測試秒殺成功:
扣減庫存成功:
四、超賣問題
當有大量請求同時存取時,就會出現超賣問題
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
1. 加锁方式 - 乐观锁
乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:
(1)版本号法
(2)CAS法
用库存代替了版本号,可以少加一个字段
扣库存时,与查询时的库存比较,没被修改则可以扣减库存
2. 乐观锁解决超卖问题
乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题:
// 之前的代码 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).update(); // 乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") // set stock = stock -1 .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); // where id = ? and stock = ?
又出现新的问题:
假设100个线程同时请求,但通过CAS判断后,只有一个线程能扣减库存成功,其余99个线程全部失败
此时,库存剩余99,但是实际业务可以满足其余99个线程扣减库存
虽然能解决超卖问题,但是设计不合理
所以为了解决失败率高的问题,需要进一步改进:
通过CAS 不再 判断前后库存是否一致,而是判断库存是否大于0
boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0
3. 小结
超卖这样的线程安全问题,解决方案有哪些?
(1)悲观锁:添加同步锁,让线程串行执行
优点:简单粗暴
缺点:性能一般
(2)乐观锁:不加锁,在更新时判断是否有其它线程在修改
优点:性能相对悲观锁好(但是仍然需要同时查数据库,影响性能)
缺点:存在成功率低的问题(可以采用分段锁方式提高成功率)
五、一人一单问题
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
在扣减库存之前,加上一人一单的逻辑:
// 5.一人一单逻辑 Long userId = UserHolder.getUser().getId(); // 5.1.查询订单数量 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); // 5.2.判断是否下过单 if (count > 0) { // 用户已经购买过了 return Result.fail("用户已经购买过一次!"); }
此处仍会出现并发问题,当同一用户模拟大量请求同时查询是否下过单时,如果正好都查询出count为0,就会跳过判断继续执行扣减库存的逻辑,此时就会出现一人下多单的问题
解决方法:
由于是判断查询的数据是否存在,而不是像之前判断查询的数据是否修改过
所以这里只能加悲观锁
1. 加锁分析
首先将一人一单之后的逻辑全部加锁,所以将一人一单之后的逻辑抽取出一个方法进行加锁,public Result createVoucherOrder(Long voucherId)
如果直接在方法上加锁,则锁的是this对象,锁的对象粒度过大,就算是不同的人执行都会阻塞住,影响性能,public synchronized Result createVoucherOrder(Long voucherId)
所以将锁的对象改为userId,但是不能直接使用synchronized (userId),因为每次执行Long userId = UserHolder.getUser().getId();虽然值一样,但是对象不同,因此需要这样加锁 synchronized (userId.toString().intern()),intern()表示每次从字符串常量池中获取,这样值相同时,对象也相同
为了防止事务还没提交就释放锁的问题,则不能将锁加在createVoucherOrder方法内部,例如:
@Transactional public Result createVoucherOrder(Long voucherId) { synchronized (userId.toString().intern()) { 。。。 } }
而是需要等事务提交完再释放锁,例如:
synchronized (userId.toString().intern()) { // 获取代理对象(事务) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }
2. 事务分析
由于只有一人一单之后的逻辑涉及到修改数据库,所以只需对该方法加事务
@Transactional
public Result createVoucherOrder(Long voucherId)
由于只对createVoucherOrder方法加了事务,而该方法是在seckillVoucher方法中被调用,seckillVoucher方法又没有加事务,为了防止事务失效,则不能直接在seckillVoucher方法调用createVoucherOrder方法,例如:
@Override public Result seckillVoucher(Long voucherId) { 。。。。 synchronized (userId.toString().intern()) { return this.createVoucherOrder(voucherId); } }
而是需要通过代理对象调用createVoucherOrder方法,因为@Transactional事务注解的原理是通过获取代理对象执行目标对象的方法,进行AOP操作,所以需要这样:
@Override public Result seckillVoucher(Long voucherId) { 。。。。 // 获取代理对象(事务) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }
并且还要引入依赖:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
还要开启注解暴露出代理对象:
@EnableAspectJAutoProxy(exposeProxy = true) @MapperScan("com.hmdp.mapper") @SpringBootApplication public class HmDianPingApplication { public static void main(String[] args) { SpringApplication.run(HmDianPingApplication.class, args); } }
完整VoucherOrderServiceImpl代码:
@Service public class VoucherOrderServiceImpl extends ServiceImplimplements IVoucherOrderService { @Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // 2.判断秒杀是否开始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀尚未开始!"); } // 3.判断秒杀是否已经结束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀已经结束!"); } // 4.判断库存是否充足 if (voucher.getStock() < 1) { // 库存不足 return Result.fail("库存不足!"); } Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()) { // 获取代理对象(事务) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder(Long voucherId) { // 5.一人一单逻辑 Long userId = UserHolder.getUser().getId(); // 5.1.查询订单数量 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); // 5.2.判断是否下过单 if (count > 0) { // 用户已经购买过了 return Result.fail("用户已经购买过一次!"); } // 6,扣减库存 // 乐观锁方式,通过CAS判断库存是否大于0,解决超卖问题: boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0 if (!success) { // 扣减库存失败 return Result.fail("库存不足!"); } // 7.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); // 7.1.订单id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); // 7.2.用户id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); // 8.返回订单id return Result.ok(orderId); } }
六、集群模式下并发安全问题
通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
我们将服务启动两份,端口分别为8081和8082:
然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
修改完后,重新加载nginx配置文件:
现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题:
访问8081端口的线程进入了synchronized中
访问8082端口的线程也进入了synchronized中
最终同一个用户下了2单扣了2个库存,所以在集群模式下,出现了一人多单的问题:
分析:
锁的原理是每个JVM中都有一个Monitor作为锁对象,所以当对象相同时,获取的就是同一把锁
但是不同的JVM中的Monitor不同,所以获取的不是同一把锁
因此集群模式下,加synchronized锁也会出现并发安全问题,需要加分布式锁
以上是Redis優惠券秒殺功能怎麼實現的詳細內容。更多資訊請關注PHP中文網其他相關文章!

Redis的數據模型和結構包括五種主要類型:1.字符串(String):用於存儲文本或二進制數據,支持原子操作。 2.列表(List):有序元素集合,適合隊列和堆棧。 3.集合(Set):無序唯一元素集合,支持集合運算。 4.有序集合(SortedSet):帶分數的唯一元素集合,適用於排行榜。 5.哈希表(Hash):鍵值對集合,適合存儲對象。

Redis的數據庫方法包括內存數據庫和鍵值存儲。 1)Redis將數據存儲在內存中,讀寫速度快。 2)它使用鍵值對存儲數據,支持複雜數據結構,如列表、集合、哈希表和有序集合,適用於緩存和NoSQL數據庫。

Redis是一個強大的數據庫解決方案,因為它提供了極速性能、豐富的數據結構、高可用性和擴展性、持久化能力以及廣泛的生態系統支持。 1)極速性能:Redis的數據存儲在內存中,讀寫速度極快,適合高並發和低延遲應用。 2)豐富的數據結構:支持多種數據類型,如列表、集合等,適用於多種場景。 3)高可用性和擴展性:支持主從復制和集群模式,實現高可用性和水平擴展。 4)持久化和數據安全:通過RDB和AOF兩種方式實現數據持久化,確保數據的完整性和可靠性。 5)廣泛的生態系統和社區支持:擁有龐大的生態系統和活躍社區,

Redis的關鍵特性包括速度、靈活性和豐富的數據結構支持。 1)速度:Redis作為內存數據庫,讀寫操作幾乎瞬時,適用於緩存和會話管理。 2)靈活性:支持多種數據結構,如字符串、列表、集合等,適用於復雜數據處理。 3)數據結構支持:提供字符串、列表、集合、哈希表等,適合不同業務需求。

Redis的核心功能是高性能的內存數據存儲和處理系統。 1)高速數據訪問:Redis將數據存儲在內存中,提供微秒級別的讀寫速度。 2)豐富的數據結構:支持字符串、列表、集合等,適應多種應用場景。 3)持久化:通過RDB和AOF方式將數據持久化到磁盤。 4)發布訂閱:可用於消息隊列或實時通信系統。

Redis支持多種數據結構,具體包括:1.字符串(String),適合存儲單一值數據;2.列表(List),適用於隊列和棧;3.集合(Set),用於存儲不重複數據;4.有序集合(SortedSet),適用於排行榜和優先級隊列;5.哈希表(Hash),適合存儲對像或結構化數據。

Redis計數器是一種使用Redis鍵值對存儲來實現計數操作的機制,包含以下步驟:創建計數器鍵、增加計數、減少計數、重置計數和獲取計數。 Redis計數器的優勢包括速度快、高並發、持久性和簡單易用。它可用於用戶訪問計數、實時指標跟踪、遊戲分數和排名以及訂單處理計數等場景。

使用 Redis 命令行工具 (redis-cli) 可通過以下步驟管理和操作 Redis:連接到服務器,指定地址和端口。使用命令名稱和參數向服務器發送命令。使用 HELP 命令查看特定命令的幫助信息。使用 QUIT 命令退出命令行工具。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境

PhpStorm Mac 版本
最新(2018.2.1 )專業的PHP整合開發工具

SecLists
SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

DVWA
Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

VSCode Windows 64位元 下載
微軟推出的免費、功能強大的一款IDE編輯器