1. 问题

需求:课程点赞功能,可以取消点赞。

可取消的点赞功能是需要记录点赞状态的,因此需要持久化 课程用户id (后面分别用 coureseIduid 代替)。
要应对用户多次的点击点赞(或取消点赞)按钮,所以可以用 redis 来协助处理,再通过定时任务 crontab 持久化到 mysql
需要考虑到有一定的并发问题。

2. 处理过程

  1. 思路1:使用 redis 的 bitmap

通俗理解:bitmap是位图的概念,可以想象成一个铁轨的俯视图,中间有若干根杆隔开,杆与杆包围的空间就是一个小格子,随机取一个格子到起点格子的格子数量就是当前格子的序号,对应uid,格子里面只能放 0 或 1 对应 点赞 与 没有点赞(或取消点赞),而不同的铁轨对应不同的课程。
那么这时再审视一次,大量的bitmap需要大量的空间,而且课程的点赞数与uid总数严重不匹配,因为点赞者比不点赞者多太多,再有是大量的不需要即时访问的点赞缓存在redis中其实是缓存在服务器的内存中的,造成内存内的使用性价比不高。

  1. 思路2:使用 redis 的 hash table

hash表可以将课程点赞以表的形式存起来,key为coursesId,field为uid,value为0或1,代表当前是否已点赞。无论用户在某个时间段点击了多少次,都不会影响效率。

  1. <?php
  2. // 点赞类
  3. class Like extends Component
  4. {
  5. const OFF = 0;
  6. const ON = 1;
  7. public static $valueMap = [
  8. 'on' => self::OFF,
  9. 'off' => self::ON
  10. ];
  11. /** @var Connection redis对象 */
  12. public $redis;
  13. // 点赞key名称
  14. public $key;
  15. // 点赞计数key名称
  16. public $countKey;
  17. public function __construct($key, $countKey, $config = [])
  18. {
  19. $this->key = $key;
  20. $this->countKey = $countKey;
  21. $this->init();
  22. parent::__construct($config);
  23. }
  24. public function init()
  25. {
  26. $this->redis = Yii::$app->redis;
  27. }
  28. // 点赞
  29. public function setRedisLike(string $field, $value)
  30. {
  31. if (!in_array($value, self::$valueMap)) {
  32. return ['status' => 9001, 'message' => '参数错误', 'data' => []];
  33. }
  34. // 点赞总数增减
  35. $amount = ($value == self::ON) ? 1 : -1;
  36. // 开启redis事务
  37. $this->redis->multi();
  38. // 记录总数
  39. $this->redis->incrby($this->countKey, $amount);
  40. // 记录
  41. $ret = $this->redis->hset($this->key, $field, $value);
  42. $this->redis->exec();
  43. return ['status' => 0, 'message' => $ret, 'data' => []];
  44. }
  45. // 获取是否点赞
  46. public function getRedisLike(string $field)
  47. {
  48. $fieldValue = $this->redis->hget($this->key, $field);
  49. $count = $this->redis->get($this->countKey);
  50. return ['status' => 0, 'message' => '', 'data' => ['field' => $fieldValue, 'count' => $count]];
  51. }
  52. }
  1. <?php
  2. // 调用点赞
  3. class FirmCollegeLikeV4Services
  4. {
  5. // 点赞key前缀
  6. private static $likeTmpRecordKeyPrefix = 'firmCollegeLikeRecordV4';
  7. // 点赞计数key名称
  8. private static $likeTmpRecordCountKeyPrefix = 'firmCollegeLikeRecordCountV4';
  9. public function __construct()
  10. {
  11. }
  12. // 点赞/取消点赞
  13. public function like(int $coursesId, int $uid, int $value)
  14. {
  15. $key = self::$likeTmpRecordKeyPrefix . ':' . $coursesId;
  16. $countKey = self::$likeTmpRecordCountKeyPrefix . ':' . $coursesId;
  17. $likeCls = new Like($key, $countKey);
  18. if (!in_array($value, Like::$valueMap)) {
  19. return ['status' => 9001, 'message' => '参数错误', 'data' => []];
  20. }
  21. return $likeCls->setRedisLike($uid, $value);
  22. }
  23. // 是否已点赞
  24. public function isLike(int $coursesId, int $uid)
  25. {
  26. $key = self::$likeTmpRecordKeyPrefix . ':' . $coursesId;
  27. $countKey = self::$likeTmpRecordCountKeyPrefix . ':' . $coursesId;
  28. $likeCls = new Like($key, $countKey);
  29. return $likeCls->getRedisLike($uid);
  30. }
  31. // 持久化
  32. public function PersistLikeRedis2Mysql()
  33. {
  34. /** @var Connection $redis */
  35. $redis = Yii::$app->redis;
  36. $keys = $redis->keys(self::$likeTmpRecordKeyPrefix . "*");
  37. /** @var array $batchInsertUserLikeDatum 批量新增到数据库点赞变量 */
  38. $batchInsertUserLikeDatum = [];
  39. /** @var array $deleteRedisKeyCoursesId 需要删除redis key的课程ids */
  40. $deleteRedisKeyCoursesId = [];
  41. foreach ($keys as $key) {
  42. // 课程Id
  43. $coursesId = substr($key, strpos($key, ':') + 1);
  44. // 获取课程
  45. $coursesModel = TblFirmCollegeCoursesDao::getRawModelById($coursesId);
  46. if ($coursesModel) {
  47. $coursesModel->is_delete && $deleteRedisKeyCoursesId[] = $coursesId;
  48. // 更新点赞数量
  49. $coursesModel->like_times = $this->getCoursesLikeCount($coursesId);
  50. $coursesModel->updated_at = time();
  51. $coursesModel->save();
  52. }
  53. // 获取该课程点赞
  54. $fields = $redis->hkeys($key);
  55. if (!$fields) {
  56. continue;
  57. }
  58. // 处理点赞
  59. foreach ($fields as $field) {
  60. // 检查是否存在
  61. $isExist = $redis->hexists($key, $field);
  62. if (!$isExist) continue;
  63. // 取值
  64. $value = $redis->hget($key, $field);
  65. // 删除
  66. $redis->hdel($key, $field);
  67. $dbLikeDTO = TblFirmCollegeCoursesUserLikeDao::getByCoursesIdUid($coursesId, $field, 'id, is_like');
  68. if (isset($dbLikeDTO['id'])) {
  69. ($dbLikeDTO['is_like'] != $value) && TblFirmCollegeCoursesUserLikeDao::updateLikeById($dbLikeDTO['id'], $value);
  70. } else {
  71. $batchInsertUserLikeDatum[] = [
  72. 'courses_id' => $coursesId,
  73. 'user_id' => $field,
  74. 'is_like' => $value,
  75. 'created_at' => time(),
  76. 'updated_at' => time(),
  77. ];
  78. }
  79. }
  80. }
  81. // 批量新增点赞记录到数据库
  82. if ($batchInsertUserLikeDatum) {
  83. TblFirmCollegeCoursesUserLikeDao::batchInsert($batchInsertUserLikeDatum);
  84. }
  85. // 课程已删除 && 删除redis key
  86. if ($deleteRedisKeyCoursesId) {
  87. foreach ($deleteRedisKeyCoursesId as $coursesId) {
  88. $recordKey = self::$likeTmpRecordKeyPrefix . ':' . $coursesId;
  89. $redis->del($recordKey);
  90. $countKey = self::$likeTmpRecordCountKeyPrefix . ':' . $coursesId;
  91. $redis->del($countKey);
  92. }
  93. }
  94. }
  95. }