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
|