Redis常见使用场景

99 篇文章 4 订阅
21 篇文章 1 订阅

在日常开发中,多多少少都在使用redis中间件,比如用作缓存,分布式锁,唯一id、消息通知等,现将用到的场景记录下来,后续会更新哦。

string类型
```
set key value //存入键值对
get key value //根据键获取值
del key //删除
expire key timeout // 为key设置一个超时时间,超过时间会自动释放
setnx key value //当且仅当key不存在时,set一个key为value的字符串,返回1;若key存在,则什么都不做,返回0
incr key //将key中存储的值加一
decr key //将key中存储的值减一
```
分布式锁
```
setnx order_lock true //返回1代表获取锁成功
setnx order_lock true //返回0代表获取锁失败
del order_lock //执行完业务释放锁
好比:张三去上厕所,看厕所门锁着,他就不进去了,厕所门开着他才进入;
问题1,若redis因为宕机或出现异常未释放锁,就造成了死锁;可以通过设置过期时间解决未释放锁的情况,但需要组合命令set key value ex seconds nx;问题2,一个业务执行时间很长,锁已经自动过期了,别人获取到了锁,但是当业务执行完之后直接释放了锁,这时就可能删除了别人的锁,可以通过在加锁的时候设置一个随机值,在删除锁的时候进行对比,若是自己的锁才删除;
redis实现分布式锁的方式为去插入一条占位数据;遇到宕机情况,redis需要等到设置的过期时间到了后自动释放锁;redis在没抢占到锁的情况时一般会去自旋获取锁;
```


实现思想
```
获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断
获取锁的时间设置一个获取的超时时间,若超过这个时间则放弃获取锁
释放锁的时候,通过UUID判断是不是该锁,若是该锁则执行delete进行锁释放
```
示例,在指定时间acquireTimeout内进行秒杀活动,每个抢到订单的需要在指定时间expire内完成支付,如下:

public class RedisLock {

    private final JedisPool jedisPool;

    public RedisLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     * 加锁
     *
     * @param keyName        锁的key
     * @param acquireTimeout 获取超时时间
     * @param expire         锁的过期时间
     * @return 锁标识
     */
    public String lockWithTimeout(String keyName, long acquireTimeout, long expire) {
        // 随机生成一个value
        String value = UUID.randomUUID().toString();
        // 锁名,即key值
        String lockKey = "lock:" + keyName;
        // 锁的超时时间,上锁后超过此时间则自动释放锁
        int lockExpire = (int) (expire / 1000);
        // 获取锁的超时时间,超过这个时间则放弃获取锁
        long end = System.currentTimeMillis() + acquireTimeout;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            while (System.currentTimeMillis() < end) {
                Long setnx = jedis.setnx(lockKey, value);
                if (setnx == 1) {
                    jedis.expire(lockKey,lockExpire);
                    return value;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return null;
    }

    /**
     * 释放锁
     *
     * @param keyName    锁的key
     * @param identifier 释放锁的标识
     * @return
     */
    public boolean releaseLock(String keyName, String identifier) {
        String lockKey = "lock:" + keyName;
        boolean retFlag = false;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            while (true) {
                // 监视lock,准备开始事务
                jedis.watch(lockKey);
                // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
                if (identifier.equals(jedis.get(lockKey))) {
                    Transaction transaction = jedis.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                jedis.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return retFlag;
    }
}
public class DemoService {

    private static JedisPool jedisPool = null;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        // 设置最大连接数
        config.setMaxTotal(200);
        // 设置最大空闲数
        config.setMaxIdle(8);
        // 设置最大等待时间
        config.setMaxWaitMillis(1000 * 100);
        jedisPool = new JedisPool(config, "127.0.0.1", 6379, 3000);
    }

    RedisLock redisLock = new RedisLock(jedisPool);

    int n = 500;

    public void seckill() {
        // 返回锁的value值,供释放锁时候进行判断
        String indentifier = null;
        indentifier = redisLock.lockWithTimeout("resource", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "获得了锁");
        if (indentifier != null) {
            // 获取到锁后才可以进行操作(只有占用厕所,才可以如厕),具体能够有几个操作需要看acquireTimeout、expire
            System.out.println(--n);
            redisLock.releaseLock("resource", indentifier);
        }
    }
}
public class DemoThread extends Thread {

    private DemoService demoService;

    public DemoThread(DemoService demoService) {
        this.demoService = demoService;
    }

    @Override
    public void run() {
        demoService.seckill();
    }

    public static void main(String[] args) {
        DemoService demoService = new DemoService();
        for (int i = 0; i < 50; i++) {
            DemoThread demoThread = new DemoThread(demoService);
            demoThread.start();
        }
    }
}

结果如下:

Thread-7获得了锁
499
Thread-49获得了锁
498
Thread-50获得了锁
497

...

springboot继承redis中实现代码如下:

@Component
public class RedisLockService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 加锁
     *
     * @param keyName        锁的key
     * @param acquireTimeout 获取超时时间
     * @param expire         锁的过期时间
     * @return 锁标识
     */
    public String lockWithTimeout(String keyName, long acquireTimeout, long expire) {
        // 随机生成一个value
        String value = UUID.randomUUID().toString();
        // 锁名,即key值
        String lockKey = "lock:" + keyName;
        // 锁的超时时间,上锁后超过此时间则自动释放锁
        int lockExpire = (int) (expire / 1000);
        // 获取锁的超时时间,超过这个时间则放弃获取锁
        long end = System.currentTimeMillis() + acquireTimeout;
        while (System.currentTimeMillis() < end) {
            Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, value, lockExpire, TimeUnit.SECONDS);
            if (ifAbsent) {
                return value;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        return null;
    }

    /**
     * 释放锁
     *
     * @param keyName    锁的key
     * @param identifier 释放锁的标识
     * @return
     */
    public boolean releaseLock(String keyName, String identifier) {
        String lockKey = "lock:" + keyName;
        boolean retFlag = false;
        Object value = redisTemplate.opsForValue().get(lockKey);
        if (identifier.equals(value)) {
            retFlag = redisTemplate.delete(keyName);
        }
        return retFlag;
    }
}
@Component
public class DemoSkillService {

    int n = 500;

    @Autowired
    private RedisLockService redisLockService;

    public void seckill() {
        // 返回锁的value值,供释放锁时候进行判断
        String indentifier = null;
        indentifier = redisLockService.lockWithTimeout("resource", 5000, 1000);
        if (indentifier != null) {
            // 获取到锁后才可以进行操作(只有占用厕所,才可以如厕),具体能够有几个操作需要看acquireTimeout、expire
            System.out.println(Thread.currentThread().getName() + "获得了锁");
            System.out.println(--n);
            redisLockService.releaseLock("resource", indentifier);
        }
    }
}

结果如下;

Thread-154获得了锁
484
Thread-153获得了锁
483
Thread-185获得了锁
482
Thread-191获得了锁
481
Thread-161获得了锁
480
Thread-196获得了锁
479
Thread-194获得了锁
478

 

redis唯一编号使用场景,在分布式开发中可以通过redis的自增自减操作生成唯一id作为主键或编码来使用,由于是顺序数组组成,后续使用方便快捷。平常开发中也会使用雪花算法生产唯一id作为主键使用。

代码如下:

public interface OrderService {

    public String orderId();
}

@Service
public class OderServiceImpl implements OrderService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public String orderId() {
        String key = "order:id";
        String prefix = getPrefix();
        Long id = redisTemplate.opsForValue().increment(key);
        System.out.println("prefix:" + prefix);
        System.out.println("id:" + prefix + id);
        return prefix + id;
    }

    private String getPrefix() {
        LocalDateTime now = LocalDateTime.now();
        int year = now.getYear();
        int month = now.getMonthValue();
        int day = now.getDayOfYear();
        // 处理
        return String.valueOf(year) + String.valueOf(month) + String.valueOf(day);
    }
}

单位测试代码部分如下:

private static final int num = 100;

    private CountDownLatch countDownLatch = new CountDownLatch(num);

    @Autowired
    private OrderService orderService;

    @Test
    void contextLoads() {
        for (int i = 0; i <num; i++) {
            Thread thread = new Thread(()-> {
                try {
                    countDownLatch.await();
                    orderService.orderId();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
            countDownLatch.countDown();
        }
    }

结果如下:
prefix:202119
id:2021191081
id:2021191079
prefix:202119
id:2021191077
prefix:202119
id:2021191076
prefix:202119
id:2021191075
id:2021191073

redis消息通知场景,部分代码如下:

@Configuration
public class RedisListenerConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        return redisMessageListenerContainer;
    }
}
@Component
public class RedisTask extends KeyExpirationEventMessageListener {
    
    public RedisTask(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 接受事件后回调
        String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
        String key = new String(message.getBody(), StandardCharsets.UTF_8);
        System.out.println("key:" + key + ",chanel:" + channel);
        // 根据key进行处理
    }
}

分布式锁应用案例:

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
    SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId;
    try {
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
        if (lockFlag) {
            // HTTP请求用户服务进行用户相关的校验
            // 用户活动校验
            
            // 库存校验
            Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
            assert stock != null;
            if (Integer.parseInt(stock.toString()) <= 0) {
                // 业务异常
            } else {
                redisTemplate.opsForHash().increment(key+":info", "stock", -1);
                // 生成订单
                // 发布订单创建成功事件
                // 构建响应VO
            }
        }
    } finally {
        // 释放锁
        stringRedisTemplate.delete("key");
        // 构建响应VO
    }
    return response;
}
//改进
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId();
    String val = UUID.randomUUID().toString();
    try {
        Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);
        if (!lockFlag) {
            // 业务异常
        }

        // 用户活动校验
        // 库存校验,基于redis本身的原子性来保证
        Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
        if (currStock < 0) { // 说明库存已经扣减完了。
            // 业务异常。
            log.error("[抢购下单] 无库存");
        } else {
            // 生成订单
            // 发布订单创建成功事件
            // 构建响应
        }
    } finally {
        distributedLocker.safedUnLock(key, val);
        // 构建响应
    }
    return response;
}
//改进
// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();

...

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;

    Long seckillId = request.getSeckillId();
    if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
        // 业务异常
    }
     // 用户活动校验
     // 库存校验
    if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
        SECKILL_FLAG_MAP.put(seckillId, false);
        // 业务异常
    }
    // 生成订单
    // 发布订单创建成功事件
    // 构建响应
    return response;
}

超卖原因:虽然采用了setnx key value [EX seconds] [PX milliseconds] [NX|XX]的方式,但是如果线程A执行的时间较长没有来得及释放,锁就过期了,此时线程B是可以获取到锁的。当线程A执行完成之后,释放锁,实际上就把线程B的锁释放掉了。这个时候,线程C又是可以获取到锁的,而此时如果线程B执行完释放锁实际上就是释放的线程C设置的锁。这是超卖的直接原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值