Redis后端缓存量大的数据库查询

需求

秒杀优化
在秒杀系统中,红色部分是可以优化的,对于地址暴露接口那块,也就是用户根据id客户端访问查看页面是否可以秒杀了,如果可以秒杀,会暴露一个动态hash接口。所有也是刷新很频繁的。

public Exposer exportSeckillUrl(long seckillId) {
    Seckill seckill = seckillDao.queryById(seckillId);
    if (seckill == null) {
        return new Exposer(false, seckillId);
    }
    Date startTime = seckill.getStartTime();
    Date endTime = seckill.getEndTime();
    Date nowTime = new Date();
    if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
        return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
    }
    // 系统时间在秒杀之内,暴露秒杀接口
    String md5 = getMD5(seckillId);
    return new Exposer(true, md5, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}

主要是要访问数据库,获取秒杀对象的信息(开始结束时间,以作判断是否进行),还有验证商品id是否是假的。
反正就是对数据库的访问是很大的。

可以先让用户去缓存redis找,找不到再去数据库找,数据库找到了的话返回用户后也顺便写入缓存。以后可以去缓找了。一致性维护可以用简单的超时缓存实现。

实现

把上面的暴露秒杀接口业务从只是从数据库获取改为如下:

 public Exposer exportSeckillUrl(long seckillId) {
    // 先访问redis缓存
    Seckill seckill = redisDao.getSeckill(seckillId);
    if (seckill == null) {
        // 缓存没有,访问数据库获取
        seckill = seckillDao.queryById(seckillId);
        if (seckill == null) {
            // 数据库也没有,假的id
            return new Exposer(false, seckillId);
        } else {
            // 数据库有,存入redis。然后下面操作处理给用户
            redisDao.putSeckill(seckill);
        }
    }
    Date startTime = seckill.getStartTime();
    Date endTime = seckill.getEndTime();
    Date nowTime = new Date();
    if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
        return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
    }
    // 系统时间在秒杀之内,暴露秒杀接口
    String md5 = getMD5(seckillId);
    return new Exposer(true, md5, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}

思路如上,先去缓存找,没有再去数据库,数据库没有就是不存在了。有的话写入缓存。然后再处理返回给用户。

Redis操作实现

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import xyz.cglzwz.entity.Seckill;

/**
 * redis操作
 * @author chgl16
 * @date 2019/7/21 14:50
 */
public class RedisDao {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JedisPool jedisPool;

    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public RedisDao(String ip, int port) {
        jedisPool = new JedisPool(ip, port);
    }

    /**
     * 从redis里获取Seckill的byte[],反序列化
     * @param seckillId
     * @return
     */
    public Seckill getSeckill(long seckillId) {
        try {
            // 从池子获取一个连接
            Jedis jedis = jedisPool.getResource();
            try {
                // 构建存储表示的key
                String key = "seckill:" + seckillId;
                // 采用比jdk提供的Serializable接口高效的序列化和反序列化
                byte[] bytes = jedis.get(key.getBytes());
                if (bytes != null) {
                    // 缓存获取到
                    // 创建一个空对象
                    Seckill seckill = schema.newMessage();
                    // 把数据赋予到空对象中
                    ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                    return seckill;
                }
            } finally {
                jedis.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * redis中没有,把seckill对象序列化成字节数组发送到redis
     * @param seckill
     * @return
     */
    public String putSeckill(Seckill seckill) {
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                // 构造key
                String key = "seckill:" + seckill.getSeckillId();
                // 序列化对象
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                // 一致性解决方案:超时缓存 1h
                int timeout = 60 * 60;
                // 缓存到redis,result为操作成功失败提示
                String result = jedis.setex(key.getBytes(), timeout, bytes);
                return result;
            } finally {
                jedis.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }
}

整合Spring的话注入一下bean,因为属性jedisPool只能构造注入完成初始化。可以如下

<!-- 构造注入redis,完成属性注入 -->
<bean id="redisDao" class="xyz.cglzwz.cache.RedisDao">
    <constructor-arg index="0" value="127.0.0.1"/>
    <constructor-arg index="1" value="6379"/>
</bean>
  1. 使用Jedis客户端,从JedisPool中getResource()获取一个连接。然后进行操作。
  2. 这里的key-value都是String,但是对连接redis操作都需要改为byte[],Redis的String是二进制安全的,对Java对象的序列化存储也很好。
  3. 这里使用的序列化方法不是jdk提供的Serializable接口,而是使用高效的protostuff,压缩率和速度更快,因此对并发更好。反序列化的过程是先创建一个空对象(没有属性值),然后用schema将从缓存中获取到的序列化的对象bytes赋进去。

依赖:

<!-- 5. redis相关-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
<!-- 高效的序列化和反序列化 -->
<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-core</artifactId>
    <version>1.1.3</version>
</dependency>
<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-runtime</artifactId>
    <version>1.1.3</version>
</dependency>
  1. 这里的一致性处理采用的是超时缓存。这种适合那种秒杀商品,数据库对应的信息很少改动。所有即便超时(这里设置了一个小时)从缓存删去,再从数据库获取更新到缓存也是可以接受的。也是维护一致性非常常用和简单的一种策略方案。

 上一篇
秒杀系统流程及并发优化分析 秒杀系统流程及并发优化分析
流程分析之前看了下慕课网的秒杀系统,也动手实战过。分析下常见的秒杀抢购和抢红包系统流程和架构优化。提供给用户的前端页面主要有商品列表页和对应的详情页。登录用户信息这里只是使用了cookie。没有另外数据库层面的存储。 1.列表页列表页面
2019-07-19
下一篇 
Spring声明式事务 Spring声明式事务
Spring声明式事务 事务是对于多个SQL执行才有必要,一个就算了。推荐使用第三种@Transactional,毕竟事务声明是很严谨重要的点。这个可读性等更好。 注解声明配置方式spring-service.xml <?xm
2019-07-14
  目录