Redis 解决实际业务问题

发表于2023-06-09 | 更新于2023-06-19

阅读量:9999+

1. 生成全局唯一 ID

在并发高或数据量特别大的情况下,必然需要一个全局唯一 ID。不同的业务场景需要不同的解决方法。这里着重讲解如何使用 Redis 来解决。

业务场景

用户抢购商品或优惠券时,抢购成功会生成订单插入数据库对应的订单表。如果使用数据库自增 ID 会有一些问题:

  • ID 的订单号规律性很强,用户可以根据规律推断出一些信息,缺乏安全性;
  • 如果数据量过大,单张表自增 ID 会受限制,性能较弱。

策略

可以写一个工具类封装一个全局 ID 生成器。全局 ID 组成如下:

  • 第一位为正负符号位
  • 第二位到中间的位置为时间戳:当前时间与定义开始时间的差值
  • 后32位为时间序列号计数器,每秒会生成大量不同的 ID
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
package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;

private StringRedisTemplate stringRedisTemplate;

public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}

public long nextId(String keyPrefix) {
// 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;

// 生成序列号
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

// 拼接并返回
return timestamp << COUNT_BITS | count;
}
}

2. 商品秒杀下单,如何解决库存超卖问题

超卖问题

热门商品在短时间内会被大量用户购买,可能导致卖出的订单数大于库存数。解决方法包括悲观锁和乐观锁。

悲观锁

  • 悲观锁:认为线程安全问题一定会发生,因此在操作数据前先获取锁,确保线程可以串行化执行。

乐观锁

  • 乐观锁:认为线程问题不一定发生,所以不获取锁,在更新数据时再判断有没有其他线程对数据做了修改,如果没有修改则更新数据,如果被修改了则重试或报异常。
乐观锁代码示例
1
2
3
4
5
// 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).eq("stock", voucher.getStock())
.update();

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
java复制代码@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 voucher = seckillVoucherService.getById(voucherId);
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始!");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已结束");
}
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) {
Long userId = UserHolder.getUser().getId();
int 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).gt("stock",0)
.update();
if(!success){
return Result.fail("库存不足!");
}
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}

4. 分布式锁

在并发情况下,是多个 JVM 的情况,而多个 JVM 中存在多个锁,此时需要并发情况下多个 JVM 也能有一个共有的锁。

基于 Redis 的分布式锁

自定义锁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class SimpleRedisLock implements ILock {

private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "locks:";

public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}

@Override
public boolean tryLock(long timeoutSec) {
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}

@Override
public void unLock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}

改造 Redis 分布式锁

使用 Lua 脚本实现原子性操作。

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
java复制代码public class SimpleRedisLock implements ILock {

private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "locks:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}

@Override
public boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}

@Override
public void unLock() {
String threadId = ID_PREFIX + Thread.currentThread().getId();
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if(threadId.equals(id)){
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}

Lua 脚本

1
2
3
4
5
lua复制代码-- Lua 脚本
if(redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('del',KEYS[1])
end
return 0

Java 调用 Lua 脚本

1
2
3
4
5
6
java复制代码public void unLock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}

5. Redisson

配置 Pom 依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>

配置 Redisson 配置类

1
2
3
4
5
6
7
8
9
10
java复制代码@Configuration
public class RedissonConfig {

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://[ip]:6379").setPassword("【redis密码】");
return Redisson.create(config);
}
}

使用 Redisson 分布式锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy