一、利用 Redis 实现全局唯一 ID 生成
(1) 为啥要用全局唯一 ID 生成
CREATE TABLE `tb_voucher_order` (
`id` bigint(20) NOT NULL COMMENT '主键',
`user_id` bigint(20) unsigned NOT NULL COMMENT '下单的用户id',
`voucher_id` bigint(20) unsigned NOT NULL COMMENT '购买的代金券id',
`pay_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
`status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
`pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
`use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
`refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
🍀 id 字段不是自增
AUTO_INCREMENT
的
- 每个店铺都可以发布优惠券:
- 用户抢购的时候会生成订单并保存到
tb_voucher_order
这张表中 - 如订单 id 使用数据库自增 ID 会出现以下问题:
🍀 id 规律性太明显(可能会被用户猜测到优惠券的 id)
🍀 受单表数据量的限制(优惠券订单可能很多,当分库分表的时候,每张表的 id 各自递增)
(2) 全局唯一 ID 生成器
🍀 全局 ID 生成器:一种在分布式系统下用来生成全局唯一 ID 的工具。一般要满足下列特性:
🍀 ① 唯一性:一个 ID 只能对应数据库中的一条记录
🍀 ② 高可用:生成 ID 的功能在高并发情况下也要能够提供服务
🍀 ③ 高性能:生成 ID 的速度要足够快(否则会影响其他业务的功能)
🍀 ④ 递增性:ID 必须递增才能让 MySQL 为表创建索引(提高数据库表查询效率的实现)
🍀 ⑤ 安全性:ID 不能过于简单,让用户猜测到
(3) 全局 ID 的结构
🍀 可使用 Redis 的 incr
实现自增
🍀 为了增加 ID 的安全性,不直接使用 Redis 自增的数值,而是拼接一些其它信息
ID 的组成部分:
🍀 符号位:1bit,永远为 0【ID 永远是正数】
🍀 时间戳:31bit,以秒为单位,可以使用69年
🍀 序列号:32bit,秒内的计数器,支持每秒产生2^32
个不同 ID
(4) 代码实现
① RedisIdWorker
@Component
@SuppressWarnings("all")
public class RedisIdWorker {
// 开始时间(秒)
private static final long BEGIN_DAY_SECONDS;
// 序列号的长度
private static final long NO_BITS = 32;
@Resource
private StringRedisTemplate stringRedisTemplate;
static {
BEGIN_DAY_SECONDS = getSecondsOfDate(2020, 5, 20, 5, 20, 20);
}
/**
* @param idPrefix 标识 ID 是哪个业务的
* @return ID 值
*/
public long newId(String idPrefix) {
// 1.生成时间戳
long seconds = getNowSeconds() - BEGIN_DAY_SECONDS;
// 2.生成序列号
// 2.1 当前日期
String ymd = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2 使用 Redis 生成序列号
Long no = stringRedisTemplate.opsForValue().increment("icrId:" + idPrefix + ":" + ymd);
// 把时间戳左移32位, 空出序列号的位置
return seconds << NO_BITS | no;
}
/**
* 获取某个日期的秒数
*/
private static long getSecondsOfDate(int y, int month, int d, int h, int min, int sec) {
LocalDateTime time = LocalDateTime.of(y, month, d, h, min, sec);
return time.toEpochSecond(ZoneOffset.UTC);
}
/**
* 获取此时此刻的秒数
*/
private static long getNowSeconds() {
LocalDateTime curTime = LocalDateTime.now();
return curTime.toEpochSecond(ZoneOffset.UTC);
}
}
② Test
@SpringBootTest
class HmDianPingApplicationTests {
@Resource
private RedisIdWorker redisIdWorker;
// 线程池
private ExecutorService executorService = Executors.newFixedThreadPool(520);
// 计数器
private CountDownLatch latch = new CountDownLatch(300);
@Test
public void testReidIdWorker() throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long orderId = redisIdWorker.newId("order");
System.out.println("orderId = " + orderId);
}
latch.countDown(); // 任务执行完就递减
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
executorService.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("duration: " + (end - begin));
}
}
(5) 全局唯一 ID 其他生成策略
全局唯一ID生成策略:
🍀 UUID
🍀 Redis 自增
🍀 snowflake 算法
🍀 数据库自增(专门用一张表自增 ID)
Redis 自增 ID 策略:
🍀 每天一个 key,方便统计订单量
🍀 ID 结构:时间戳 + 计数器
二、添加优惠券
(1) 数据库
普通券表:
CREATE TABLE `tb_voucher` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`shop_id` bigint(20) unsigned DEFAULT NULL COMMENT '商铺id',
`title` varchar(255) NOT NULL COMMENT '代金券标题',
`sub_title` varchar(255) DEFAULT NULL COMMENT '副标题',
`rules` varchar(1024) DEFAULT 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 NOT NULL DEFAULT '0' COMMENT '0,普通券;1,秒杀券',
`status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '1,上架; 2,下架; 3,过期',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
```秒杀券表:`
CREATE TABLE `tb_seckill_voucher` (
`voucher_id` bigint(20) unsigned NOT NULL COMMENT '关联的优惠券的id',
`stock` int(8) NOT NULL COMMENT '库存',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`begin_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
`end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '失效时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='秒杀优惠券表,与优惠券是一对一关系'
(2) 添加优惠券接口
📗 优惠券(或秒杀券)增加完毕后会返回券的 ID
@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);
}
{
"actualValue": 10000,
"rules": "全场通用\\n无需预约\\n可无限叠加\\n不兑现、不找零七\\n仅堂食",
"updateTime": "2022-05-02T10:10:10",
"title": "100元代金券(大优惠)",
"type": 1,
"payValue": 8000,
"subTitle": "错过再等一年",
"createTime": "2022-05-10T10:10:10",
"id": 1,
"shopId": 1,
"beginTime": "2023-08-20T10:10:10",
"endTime": "2023-08-21T23:10:10",
"stock": 100,
"status": 1
}
三、优惠券秒杀下单功能
📖 ① 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
📖 ② 库存是否充足,库存不足则无法下单
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional // 事务
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucherById = seckillVoucherService.getById(voucherId);
// 判断秒杀是否开始或结束
LocalDateTime beginTime = voucherById.getBeginTime();
LocalDateTime endTime = voucherById.getEndTime();
LocalDateTime nowTime = LocalDateTime.now();
if (nowTime.isBefore(beginTime)) {
return Result.fail("秒杀未开始(no start)");
}
if (nowTime.isAfter(endTime)) {
return Result.fail("秒杀已结束(finish)");
}
// 判断库存是否充足
Integer stock = voucherById.getStock();
if (stock < 1) {
return Result.fail("库存不足");
}
// 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
if (success) {
VoucherOrder voucherOrder = new VoucherOrder();
long seckillOrderId = redisIdWorker.newId("seckillOrder");
voucherOrder.setId(seckillOrderId); // 订单 ID
voucherOrder.setUserId(UserHolder.getUser().getId()); // 用户 ID
voucherOrder.setVoucherId(voucherId); // 优惠券 ID
if (save(voucherOrder)) {
return Result.ok(seckillOrderId);
}
return Result.fail("服务器忙, 请稍后再秒杀下单");
}
return Result.fail("库存不足");
}
}
(1) 超卖问题
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁: 超卖问题:
悲观锁
📖 认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行
📖 如Synchronized、Lock
都属于悲观锁
乐观锁
📖 认为线程安全问题不一定会发生,因此不加锁
📖 在更新数据时去判断有没有其它线程对数据做了修改。
📖 如果没有修改则认为是安全的,自己才更新数据
📖 如果已经被其它线程修改,说明发生了安全问题,此时可以重试或异常
(2) 乐观锁(版本号和 CAS)
乐观锁的关键是判断之前查询得到的数据是否有被修改过
(3) 乐观锁解决超卖问题
四、一人一单功能【☆】
📖 修改秒杀业务,要求同一个优惠券,同一个用户只能下一单
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucherById = seckillVoucherService.getById(voucherId);
// 判断秒杀是否开始或结束
LocalDateTime beginTime = voucherById.getBeginTime();
LocalDateTime endTime = voucherById.getEndTime();
LocalDateTime nowTime = LocalDateTime.now();
if (nowTime.isBefore(beginTime)) {
return Result.fail("秒杀未开始(no start)");
}
if (nowTime.isAfter(endTime)) {
return Result.fail("秒杀已结束(finish)");
}
// 判断库存是否充足
Integer stock = voucherById.getStock();
if (stock < 1) {
return Result.fail("库存不足");
}
// 一人一单
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
// 获取事务的代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(userId, voucherId);
}
}
@Transactional // 事务
public Result createVoucherOrder(Long userId, Long voucherId) {
Integer count = query().eq("user_id", userId)
.eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("一人只能下一单");
}
// 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.ge("stock", 0) // 保证库存大于零(CAS 乐观锁)
.update();
if (success) {
VoucherOrder voucherOrder = new VoucherOrder();
long seckillOrderId = redisIdWorker.newId("seckillOrder");
voucherOrder.setId(seckillOrderId); // 订单 ID
voucherOrder.setUserId(userId); // 用户 ID
voucherOrder.setVoucherId(voucherId); // 优惠券 ID
if (save(voucherOrder)) {
return Result.ok(seckillOrderId);
}
return Result.fail("服务器忙, 请稍后再秒杀下单");
}
return Result.fail("库存不足");
}
}
五、并发情况下的线程安全问题
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/json;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
# 指定前端项目所在的位置
location / {
root html/hmdp;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location /api {
default_type application/json;
#internal;
keepalive_timeout 30s;
keepalive_requests 1000;
#支持keep-alive
proxy_http_version 1.1;
rewrite /api(/.*) $1 break;
proxy_pass_request_headers on;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
#proxy_pass http://127.0.0.1:8081;
proxy_pass http://backend;
}
}
upstream backend {
server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
}
}
文章来源:https://www.toymoban.com/news/detail-587913.html
集群模式下,每个 JVM 都有自己的锁监视器
每个 JVM 的锁监视器互相不可见文章来源地址https://www.toymoban.com/news/detail-587913.html
到了这里,关于【Redis】4、全局唯一 ID生成、单机(非分布式)情况下的秒杀和一人一单的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!