Redis常见使用场景
发布日期:2021-05-27 02:54:09 浏览次数:17 分类:技术文章

本文共 12339 字,大约阅读时间需要 41 分钟。

在日常开发中,多多少少都在使用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 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中实现代码如下:

@Componentpublic 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;    }}
@Componentpublic 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();}@Servicepublic 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 
{ 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消息通知场景,部分代码如下:

@Configurationpublic class RedisListenerConfig {    @Autowired    private RedisConnectionFactory redisConnectionFactory;    @Bean    public RedisMessageListenerContainer redisMessageListenerContainer() {        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);        return redisMessageListenerContainer;    }}
@Componentpublic 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
SECKILL_FLAG_MAP = new ConcurrentHashMap<>();// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMapprivate static Map
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设置的锁。这是超卖的直接原因。

转载地址:https://blog.csdn.net/leijie0322/article/details/112394653 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:oracle的rownum和mysql的limit浅析
下一篇:Java常用设计模式再相识

发表评论

最新留言

很好
[***.229.124.182]2024年09月04日 13时33分43秒