Redis 解决实际业务问题
发表于2023-06-09 | 更新于2023-06-19
阅读量:9999+
1. 生成全局唯一 ID
在并发高或数据量特别大的情况下,必然需要一个全局唯一 ID。不同的业务场景需要不同的解决方法。这里着重讲解如何使用 Redis 来解决。
业务场景
用户抢购商品或优惠券时,抢购成功会生成订单插入数据库对应的订单表。如果使用数据库自增 ID 会有一些问题:
- ID 的订单号规律性很强,用户可以根据规律推断出一些信息,缺乏安全性;
- 如果数据量过大,单张表自增 ID 会受限制,性能较弱。
策略
可以写一个工具类封装一个全局 ID 生成器。全局 ID 组成如下:
- 第一位为正负符号位
- 第二位到中间的位置为时间戳:当前时间与定义开始时间的差值
- 后32位为时间序列号计数器,每秒会生成大量不同的 ID
| 12
 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. 商品秒杀下单,如何解决库存超卖问题
超卖问题
热门商品在短时间内会被大量用户购买,可能导致卖出的订单数大于库存数。解决方法包括悲观锁和乐观锁。
悲观锁
- 悲观锁:认为线程安全问题一定会发生,因此在操作数据前先获取锁,确保线程可以串行化执行。
乐观锁
- 乐观锁:认为线程问题不一定发生,所以不获取锁,在更新数据时再判断有没有其他线程对数据做了修改,如果没有修改则更新数据,如果被修改了则重试或报异常。
乐观锁代码示例
| 12
 3
 4
 5
 
 | boolean success = seckillVoucherService.update()
 .setSql("stock = stock - 1")
 .eq("voucher_id", voucherId).eq("stock", voucher.getStock())
 .update();
 
 | 
3. 一人一单问题
同一个优惠券,一个用户只能下一单。在扣减订单操作前需要判断该用户是否购买过这一商品。
| 12
 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复制代码@Servicepublic 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 的分布式锁
自定义锁实现
| 12
 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 脚本实现原子性操作。
| 12
 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 脚本
| 12
 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 脚本
| 12
 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 依赖
| 12
 3
 4
 5
 
 | xml复制代码<dependency><groupId>org.redisson</groupId>
 <artifactId>redisson</artifactId>
 <version>3.13.6</version>
 </dependency>
 
 | 
配置 Redisson 配置类
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | java复制代码@Configurationpublic class RedissonConfig {
 
 @Bean
 public RedissonClient redissonClient() {
 Config config = new Config();
 config.useSingleServer().setAddress("redis://[ip]:6379").setPassword("【redis密码】");
 return Redisson.create(config);
 }
 }
 
 | 
使用 Redisson 分布式锁
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | java复制代码@Servicepublic 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
 
 |